@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
@@ -8,6 +8,7 @@
8
8
  * valven-gate.mjs --mode dev|commit|review
9
9
  * [--repo <dir>] [--base <ref>] [--head <ref> | --worktree] [--branch <name>]
10
10
  * [--diff-file <patch>] [--commits-file <path>] [--pr-body-file <path>]
11
+ * [--pr-meta-file <json>]
11
12
  * [--rules <rules.yml>] [--prefs <prefs.json>] [--now <ISO date>]
12
13
  * [--out <file>]
13
14
  *
@@ -20,6 +21,12 @@
20
21
  * (one subject per line), --branch and --pr-body-file when known. The rework
21
22
  * ratio needs history, so it is reported `not-measured`.
22
23
  *
24
+ * --pr-meta-file adds the pull request's timing (`gh pr view --json
25
+ * author,closedAt,createdAt,isDraft,mergedAt,reviewRequests,reviews`, or the normalized
26
+ * shape normalizePrMeta() documents). Without it the pickup and review durations
27
+ * are `not-measured`; coding time is measured up to now in commit mode, where the
28
+ * pull request is about to open.
29
+ *
23
30
  * A rule that cannot be measured is `not-measured` with the reason, never a pass.
24
31
  * Generated output, lockfiles and snapshots are removed with the same pattern
25
32
  * list the reviewers use (review-file-filter.mjs), so no budget counts them.
@@ -40,10 +47,12 @@
40
47
  import { execFileSync } from "node:child_process";
41
48
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
42
49
  import { join } from "node:path";
50
+ import { homedir } from "node:os";
43
51
  import { loadPatterns, partition } from "./review-file-filter.mjs";
44
52
  import { unquotePathIfNeeded } from "./git-path.mjs";
45
53
  import { runMain } from "../lib/fatal.mjs";
46
54
  import { invokedDirectly } from "../lib/invoked-directly.mjs";
55
+ import { pluginSkillRootCandidates } from "../lib/plugin-skill-roots.mjs";
47
56
  import { messages, outputLanguage, prefsPath } from "../lib/user-facing.mjs";
48
57
 
49
58
  export const CRITERIA_SOURCE = "valven-rules";
@@ -56,16 +65,29 @@ const SPLIT_RULES = new Set(["VLV-SIZE-01", "VLV-SIZE-02", "VLV-SCOPE-01"]);
56
65
  const DEFAULT_PATTERNS = join(import.meta.dirname, "..", "schemas", "review-file-exclusions.json");
57
66
 
58
67
  /**
59
- * Candidate locations of rules.yml relative to this script, installed layouts
60
- * first: Claude Code and Copilot (`<root>/skills`), Codex
61
- * (`<root>/multi-agent-refs/skills`), then the source checkout.
68
+ * Candidate locations of rules.yml. The gate reads the registry that was
69
+ * installed with it, so its evaluators and its rules come from one release: on
70
+ * Claude Code `<root>/multi-agent-refs/registries/valven-rules` (the installer
71
+ * lays it down there because Claude Code loads the skill itself from the
72
+ * ai-common-toolkit plugin), on Copilot `<root>/skills`, on Codex
73
+ * `<root>/multi-agent-refs/skills`, and in a checkout the source. Searching the
74
+ * registries dir before `<root>/skills` also keeps a `~/.claude/skills/valven-rules`
75
+ * left by an older install from being read. The plugin copies come last, for a
76
+ * layout that has none of these.
77
+ *
78
+ * @param {string} [dir] directory of this script
79
+ * @param {string} [home]
62
80
  */
63
- export function rulesCandidates(dir = import.meta.dirname) {
81
+ export function rulesCandidates(dir = import.meta.dirname, home = homedir()) {
64
82
  const rel = ["valven-rules", "references", "rules.yml"];
65
83
  return [
84
+ join(dir, "..", "multi-agent-refs", "registries", ...rel),
66
85
  join(dir, "..", "skills", ...rel),
67
86
  join(dir, "..", "multi-agent-refs", "skills", ...rel),
68
- join(dir, "..", "skills", "shared", "core", ...rel),
87
+ join(dir, "..", "skills", "shared", "external", ...rel),
88
+ ...pluginSkillRootCandidates(home, ["ai-common-toolkit"]).map((root) =>
89
+ join(root, "knowledge", ...rel),
90
+ ),
69
91
  ];
70
92
  }
71
93
 
@@ -246,6 +268,102 @@ export function anchorFile(human) {
246
268
  return [...pool].sort((a, b) => b.added + b.deleted - (a.added + a.deleted))[0].path;
247
269
  }
248
270
 
271
+ /**
272
+ * The Valven score band of a duration: 100 under `full_under_minutes`, 75 under
273
+ * `three_quarter_under_minutes`, 50 under `half_under_minutes`, otherwise 0.
274
+ * Each bound is exclusive, as in Valven's table ("under 30 min", "30 min - 2.5 hours").
275
+ *
276
+ * @param {number} minutes
277
+ * @param {{full_under_minutes:number, three_quarter_under_minutes:number, half_under_minutes:number}} rule
278
+ */
279
+ export function bandOf(minutes, rule) {
280
+ if (minutes < rule.full_under_minutes) return 100;
281
+ if (minutes < rule.three_quarter_under_minutes) return 75;
282
+ if (minutes < rule.half_under_minutes) return 50;
283
+ return 0;
284
+ }
285
+
286
+ const isoOrNull = (v) => {
287
+ if (!v) return null;
288
+ const d = new Date(v);
289
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
290
+ };
291
+
292
+ /** The earliest valid date that is not before `floor`, or null. */
293
+ const earliest = (dates, floor) => {
294
+ const valid = dates
295
+ .map(isoOrNull)
296
+ .filter((d) => d && d >= floor)
297
+ .sort();
298
+ return valid[0] ?? null;
299
+ };
300
+
301
+ /**
302
+ * Pull request timing in one shape. Accepts `gh pr view --json
303
+ * author,closedAt,createdAt,isDraft,mergedAt,reviewRequests,reviews` as is, or the
304
+ * normalized form a Bitbucket caller writes: `{createdAt, readyAt, isDraft,
305
+ * reviewerCount, firstReviewAt, approvedAt, mergedAt, closedAt}`. Reviews by the pull
306
+ * request's own author, bots and pending reviews never count, and a review left
307
+ * before the pull request was ready for review (or before it opened) is not the
308
+ * first review: pickup and review time start at ready. A reviewer count the input does not carry
309
+ * stays null, so it reads as unknown rather than as "no reviewer". Returns null
310
+ * without a valid `createdAt`, because every duration here is anchored to it.
311
+ *
312
+ * @param {object|null} raw
313
+ */
314
+ export function normalizePrMeta(raw) {
315
+ if (!raw || typeof raw !== "object") return null;
316
+ const createdAt = isoOrNull(raw.createdAt);
317
+ if (!createdAt) return null;
318
+ const base = {
319
+ createdAt,
320
+ readyAt: isoOrNull(raw.readyAt),
321
+ isDraft: raw.isDraft === true,
322
+ mergedAt: isoOrNull(raw.mergedAt),
323
+ closedAt: isoOrNull(raw.closedAt),
324
+ };
325
+ const floor = base.readyAt ?? createdAt;
326
+ if (Array.isArray(raw.reviews) || Array.isArray(raw.reviewRequests)) {
327
+ const author = raw.author?.login ?? null;
328
+ const isBot = (who) => who?.is_bot === true || /\[bot\]$/.test(who?.login ?? "");
329
+ const reviews = (raw.reviews ?? []).filter(
330
+ (r) => r?.author?.login !== author && r?.state !== "PENDING" && !isBot(r?.author),
331
+ );
332
+ const people = new Set([
333
+ ...(raw.reviewRequests ?? []).map((r) => r?.login ?? r?.name ?? r?.slug).filter(Boolean),
334
+ ...reviews.map((r) => r.author?.login).filter(Boolean),
335
+ ]);
336
+ return {
337
+ ...base,
338
+ reviewerCount: people.size,
339
+ firstReviewAt: earliest(
340
+ reviews.map((r) => r.submittedAt),
341
+ floor,
342
+ ),
343
+ approvedAt: earliest(
344
+ reviews.filter((r) => r.state === "APPROVED").map((r) => r.submittedAt),
345
+ floor,
346
+ ),
347
+ };
348
+ }
349
+ return {
350
+ ...base,
351
+ reviewerCount: Number.isFinite(raw.reviewerCount) ? raw.reviewerCount : null,
352
+ firstReviewAt: earliest([raw.firstReviewAt], floor),
353
+ approvedAt: earliest([raw.approvedAt], floor),
354
+ };
355
+ }
356
+
357
+ const minutesBetween = (from, to) =>
358
+ Math.max(0, Math.floor((new Date(to).getTime() - new Date(from).getTime()) / 60000));
359
+
360
+ function duration(minutes, lang) {
361
+ const tr = lang === "tr";
362
+ if (minutes < 60) return tr ? `${minutes} dakika` : `${minutes} min`;
363
+ const hours = Math.round((minutes / 60) * 10) / 10;
364
+ return tr ? `${String(hours).replace(".", ",")} saat` : `${hours} h`;
365
+ }
366
+
249
367
  const TEXT = {
250
368
  en: {
251
369
  size: (n, max) =>
@@ -270,6 +388,20 @@ const TEXT = {
270
388
  `The branch is ${n} commits behind its base and its merge-base is ${d} days old. Carry the change onto a fresh branch with valven-port.`,
271
389
  foreign: (s, key) =>
272
390
  `Commit "${s}" belongs to another issue (branch key ${key}). Move it to its own PR.`,
391
+ coding: (d, band) =>
392
+ `Coding took ${d} from the first commit to opening the pull request (Valven score ${band}%). Open the pull request as soon as the first slice works, and keep slices small.`,
393
+ pickup: (d, band, waiting) =>
394
+ waiting
395
+ ? `The pull request has waited ${d} for its first review (Valven score ${band}%). Remind the reviewers or ask the team for one.`
396
+ : `The first review came ${d} after the pull request was ready (Valven score ${band}%).`,
397
+ review: (d, band, open) =>
398
+ open
399
+ ? `The pull request has been in review for ${d} without an approval (Valven score ${band}%). Answer every comment in one round and push the fixes together.`
400
+ : `Review took ${d} from the first review to approval (Valven score ${band}%). Answer every comment in one round and push the fixes together.`,
401
+ noReviewer:
402
+ "The pull request has no reviewer, so nobody is asked to review it. Assign the default reviewers.",
403
+ draft: (d) =>
404
+ `The pull request has been a draft for ${d}. Mark it ready for review once its gates pass, or close it.`,
273
405
  },
274
406
  tr: {
275
407
  size: (n, max) =>
@@ -294,6 +426,19 @@ const TEXT = {
294
426
  `Branch base'in ${n} commit gerisinde ve merge-base'i ${d} günlük. Değişikliği valven-port ile güncel base'den açılan yeni bir branch'e taşı.`,
295
427
  foreign: (s, key) =>
296
428
  `"${s}" commit'i başka bir işe ait (branch ID'si ${key}). Onu ayrı bir PR'a taşı.`,
429
+ coding: (d, band) =>
430
+ `İlk commit'ten PR'ın açılmasına kadar ${d} geçmiş (Valven puanı %${band}). İlk dilim çalışır hale gelince PR'ı hemen aç, dilimleri küçük tut.`,
431
+ pickup: (d, band, waiting) =>
432
+ waiting
433
+ ? `PR ${d} süredir ilk review'u bekliyor (Valven puanı %${band}). Reviewer'lara hatırlat ya da takımdan birini iste.`
434
+ : `İlk review, PR hazır olduktan ${d} sonra geldi (Valven puanı %${band}).`,
435
+ review: (d, band, open) =>
436
+ open
437
+ ? `PR ${d} süredir review'da ve onay almadı (Valven puanı %${band}). Bütün yorumları tek turda yanıtla, düzeltmeleri birlikte push'la.`
438
+ : `İlk review'dan onaya kadar ${d} geçmiş (Valven puanı %${band}). Bütün yorumları tek turda yanıtla, düzeltmeleri birlikte push'la.`,
439
+ noReviewer: "PR'da reviewer yok, kimseden review istenmiyor. Varsayılan reviewer'ları ekle.",
440
+ draft: (d) =>
441
+ `PR ${d} süredir draft. Gate'leri geçtiyse review'a hazır olarak işaretle ya da kapat.`,
297
442
  },
298
443
  };
299
444
 
@@ -308,7 +453,9 @@ const TEXT = {
308
453
  * @param {string[]|null} input.commits subjects, null when unknown
309
454
  * @param {string|null} input.prBody null when unknown
310
455
  * @param {{ratio:number, recent:number, removed:number}|{reason:string}|null} input.rework
311
- * @param {{oldestDays:number|null, behind:number|null, baseDays:number|null}|null} [input.history]
456
+ * @param {{oldestDays:number|null, oldestCommitAt?:Date|null, behind:number|null, baseDays:number|null}|null} [input.history]
457
+ * @param {ReturnType<typeof normalizePrMeta>} [input.prMeta] null when no pull request is known
458
+ * @param {Date} [input.now]
312
459
  * @param {string} [input.lang]
313
460
  */
314
461
  export function evaluate({
@@ -320,6 +467,8 @@ export function evaluate({
320
467
  prBody,
321
468
  rework,
322
469
  history = null,
470
+ prMeta = null,
471
+ now = new Date(),
323
472
  lang = "en",
324
473
  }) {
325
474
  const t = messages(TEXT, lang);
@@ -337,7 +486,19 @@ export function evaluate({
337
486
  criteriaSource: CRITERIA_SOURCE,
338
487
  tag: TAG,
339
488
  });
340
- const record = (rule, status, detail) => results.push({ ruleId: rule.id, status, detail });
489
+ const record = (rule, status, detail, score) =>
490
+ results.push(
491
+ score === undefined
492
+ ? { ruleId: rule.id, status, detail }
493
+ : { ruleId: rule.id, status, detail, score },
494
+ );
495
+ const scoreDuration = (rule, minutes, issue, detail) => {
496
+ const band = bandOf(minutes, rule);
497
+ if (band <= 50) {
498
+ add(rule, rule.severity, issue(duration(minutes, lang), band));
499
+ record(rule, "violated", detail, band);
500
+ } else record(rule, "pass", detail, band);
501
+ };
341
502
 
342
503
  for (const rule of rules) {
343
504
  const modes = Array.isArray(rule.modes) ? rule.modes : [];
@@ -425,8 +586,14 @@ export function evaluate({
425
586
  record(rule, "not-measured", rework?.reason ?? "no history");
426
587
  break;
427
588
  }
589
+ const score = Math.round((1 - rework.ratio) * 100);
428
590
  if (rework.removed < rule.min_removed_lines) {
429
- record(rule, "pass", `${rework.removed} removed lines, below ${rule.min_removed_lines}`);
591
+ record(
592
+ rule,
593
+ "pass",
594
+ `${rework.removed} removed lines, below ${rule.min_removed_lines}`,
595
+ score,
596
+ );
430
597
  } else if (rework.ratio > rule.ratio_over) {
431
598
  const pct = Math.round(rework.ratio * 100);
432
599
  add(
@@ -434,8 +601,8 @@ export function evaluate({
434
601
  rule.severity,
435
602
  t.rework(pct, Math.round(rule.ratio_over * 100), rule.window_days),
436
603
  );
437
- record(rule, "violated", `${rework.recent}/${rework.removed}`);
438
- } else record(rule, "pass", `${rework.recent}/${rework.removed}`);
604
+ record(rule, "violated", `${rework.recent}/${rework.removed}`, score);
605
+ } else record(rule, "pass", `${rework.recent}/${rework.removed}`, score);
439
606
  break;
440
607
  }
441
608
  case "VLV-PRDESC-01": {
@@ -503,6 +670,97 @@ export function evaluate({
503
670
  } else record(rule, "pass", `${n} <= ${rule.important_over}`);
504
671
  break;
505
672
  }
673
+ case "VLV-CYCLE-01": {
674
+ const start = history?.oldestCommitAt ?? null;
675
+ const end = prMeta ? prMeta.createdAt : mode === "commit" ? now : null;
676
+ if (!start) {
677
+ record(rule, "not-measured", "commit dates unknown");
678
+ break;
679
+ }
680
+ if (!end) {
681
+ record(rule, "not-measured", "no pull request opening time (branch input)");
682
+ break;
683
+ }
684
+ const m = minutesBetween(start, end);
685
+ scoreDuration(rule, m, t.coding, `${m} min to open`);
686
+ break;
687
+ }
688
+ case "VLV-CYCLE-02": {
689
+ if (!prMeta) {
690
+ record(rule, "not-measured", "no pull request metadata");
691
+ break;
692
+ }
693
+ const ready = prMeta.readyAt ?? (prMeta.isDraft ? null : prMeta.createdAt);
694
+ if (!ready) {
695
+ record(rule, "not-measured", "still a draft; the pickup clock has not started");
696
+ break;
697
+ }
698
+ if (!prMeta.firstReviewAt && (prMeta.mergedAt || prMeta.closedAt)) {
699
+ record(
700
+ rule,
701
+ "not-measured",
702
+ prMeta.mergedAt ? "merged without a review" : "closed without a review",
703
+ );
704
+ break;
705
+ }
706
+ const waiting = !prMeta.firstReviewAt;
707
+ const m = minutesBetween(ready, prMeta.firstReviewAt ?? now);
708
+ scoreDuration(
709
+ rule,
710
+ m,
711
+ (d, band) => t.pickup(d, band, waiting),
712
+ waiting ? `${m} min, still waiting` : `${m} min to first review`,
713
+ );
714
+ break;
715
+ }
716
+ case "VLV-CYCLE-03": {
717
+ if (!prMeta?.firstReviewAt) {
718
+ record(rule, "not-measured", prMeta ? "no review yet" : "no pull request metadata");
719
+ break;
720
+ }
721
+ const done = prMeta.approvedAt ?? prMeta.mergedAt ?? prMeta.closedAt;
722
+ const m = minutesBetween(prMeta.firstReviewAt, done ?? now);
723
+ scoreDuration(
724
+ rule,
725
+ m,
726
+ (d, band) => t.review(d, band, !done),
727
+ done ? `${m} min to approval` : `${m} min, still in review`,
728
+ );
729
+ break;
730
+ }
731
+ case "VLV-PICKUP-01": {
732
+ if (!prMeta) {
733
+ record(rule, "not-measured", "no pull request metadata");
734
+ break;
735
+ }
736
+ if (prMeta.mergedAt || prMeta.closedAt) {
737
+ record(rule, "not-applicable", prMeta.mergedAt ? "already merged" : "closed");
738
+ break;
739
+ }
740
+ const problems = [];
741
+ const reviewerUnknown =
742
+ prMeta.reviewerCount === null && !prMeta.firstReviewAt && !prMeta.isDraft;
743
+ if (prMeta.reviewerCount === 0 && !prMeta.firstReviewAt && !prMeta.isDraft) {
744
+ add(rule, rule.severity, t.noReviewer);
745
+ problems.push("no reviewer");
746
+ }
747
+ const draftFor = minutesBetween(prMeta.createdAt, now);
748
+ if (prMeta.isDraft && draftFor > rule.draft_minutes_over) {
749
+ add(rule, rule.draft_severity ?? rule.severity, t.draft(duration(draftFor, lang)));
750
+ problems.push(`draft for ${draftFor} min`);
751
+ }
752
+ if (problems.length) record(rule, "violated", problems.join(", "));
753
+ else if (reviewerUnknown) record(rule, "not-measured", "reviewer count unknown");
754
+ else
755
+ record(
756
+ rule,
757
+ "pass",
758
+ prMeta.reviewerCount === null
759
+ ? "already reviewed"
760
+ : `${prMeta.reviewerCount} reviewer(s)`,
761
+ );
762
+ break;
763
+ }
506
764
  default:
507
765
  record(rule, "not-measured", "rule has no evaluator in this version of the gate");
508
766
  }
@@ -510,7 +768,10 @@ export function evaluate({
510
768
 
511
769
  const splitRequired = results.some((r) => SPLIT_RULES.has(r.ruleId) && r.status === "violated");
512
770
  const failing = findings.some((f) => f.severity === "blocking" || f.severity === "important");
513
- return { results, findings, splitRequired, failing };
771
+ const scores = Object.fromEntries(
772
+ results.filter((r) => r.score !== undefined).map((r) => [r.ruleId, r.score]),
773
+ );
774
+ return { results, findings, splitRequired, failing, scores };
514
775
  }
515
776
 
516
777
  // ---------------------------------------------------------------------------
@@ -745,7 +1006,23 @@ async function main() {
745
1006
  }
746
1007
  const oldest = commitDates.length ? Math.min(...commitDates.map((d) => d.getTime())) : null;
747
1008
  history.oldestDays = oldest === null ? null : daysBetween(new Date(oldest), now);
1009
+ history.oldestCommitAt = oldest === null ? null : new Date(oldest);
748
1010
  const prBody = args["pr-body-file"] ? readFileSync(args["pr-body-file"], "utf-8") : null;
1011
+ let prMeta = null;
1012
+ if (args["pr-meta-file"]) {
1013
+ let problem = null;
1014
+ try {
1015
+ prMeta = normalizePrMeta(JSON.parse(readFileSync(args["pr-meta-file"], "utf-8")));
1016
+ if (!prMeta) problem = "no valid createdAt";
1017
+ } catch (err) {
1018
+ problem = err.message;
1019
+ }
1020
+ if (problem) {
1021
+ process.stderr.write(
1022
+ `valven-gate: --pr-meta-file ${args["pr-meta-file"]}: ${problem}; the timeline rules report not-measured\n`,
1023
+ );
1024
+ }
1025
+ }
749
1026
 
750
1027
  const metrics = measure(files, patterns);
751
1028
  const verdict = evaluate({
@@ -757,6 +1034,8 @@ async function main() {
757
1034
  prBody,
758
1035
  rework,
759
1036
  history,
1037
+ prMeta,
1038
+ now,
760
1039
  lang: args.lang ?? outputLanguage(prefsFile),
761
1040
  });
762
1041
 
@@ -775,6 +1054,8 @@ async function main() {
775
1054
  excludedFiles: metrics.excluded.length,
776
1055
  rework: "reason" in rework ? null : rework,
777
1056
  },
1057
+ prMeta,
1058
+ scores: verdict.scores,
778
1059
  results: verdict.results,
779
1060
  findings: verdict.findings,
780
1061
  splitRequired: verdict.splitRequired,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-02T10:19:25Z",
4
- "skillCount": 221,
3
+ "generatedAt": "2026-10-07T15:02:16Z",
4
+ "skillCount": 223,
5
5
  "entries": [
6
6
  {
7
7
  "path": "shared/core/apple-archive-compliance/SKILL.md",
@@ -39,6 +39,10 @@
39
39
  "path": "shared/core/multi-agent-autopilot/SKILL.md",
40
40
  "sha256": "a9731771fbc1f45260f214f069b976eff16e7b25e2f1375b0d9a213575271bc6"
41
41
  },
42
+ {
43
+ "path": "shared/core/multi-agent-bug-bash/SKILL.md",
44
+ "sha256": "a44ad2520ef24444b68d9040519f82498abda3ebb6467890bb6b5e1460299400"
45
+ },
42
46
  {
43
47
  "path": "shared/core/multi-agent-build-optimize/SKILL.md",
44
48
  "sha256": "8534891d1198597a0893d131e6b944d15443f62b24cb5584c486a235845fd281"
@@ -161,7 +165,7 @@
161
165
  },
162
166
  {
163
167
  "path": "shared/core/multi-agent-review/SKILL.md",
164
- "sha256": "cd500e32efdf6c8f36da5bb2e43d2558d096e33cd3274aead5118dd2c14f118a"
168
+ "sha256": "2b6934d6086a467bdf145b326061ab4b83d5479bfb1a2fb36420ff8012a02e5d"
165
169
  },
166
170
  {
167
171
  "path": "shared/core/multi-agent-route-off/SKILL.md",
@@ -209,7 +213,7 @@
209
213
  },
210
214
  {
211
215
  "path": "shared/core/multi-agent-setup/SKILL.md",
212
- "sha256": "775ade509ec312055205d28f187f6c67f0cfd89cdd341a412101770526057291"
216
+ "sha256": "28c00a0a32b5d636240ced308983b4aaee919016693a686a18ccb0e1e9103ec3"
213
217
  },
214
218
  {
215
219
  "path": "shared/core/multi-agent-stack/SKILL.md",
@@ -229,7 +233,7 @@
229
233
  },
230
234
  {
231
235
  "path": "shared/core/multi-agent-sync/SKILL.md",
232
- "sha256": "d0db9f860bbc9765d25a961866fa9307bd7343d862bf71dcfbc73730837afbde"
236
+ "sha256": "b34c0e3bd9160552b9222d448e54be14504761597ceea875f0447a227df90772"
233
237
  },
234
238
  {
235
239
  "path": "shared/core/multi-agent-test-accessibility/SKILL.md",
@@ -261,16 +265,12 @@
261
265
  },
262
266
  {
263
267
  "path": "shared/core/multi-agent-update/SKILL.md",
264
- "sha256": "d96e18b70bab0a601057f75f212dd4fb563db0cb250607c770ce1c7a1df4b1d0"
268
+ "sha256": "37f5950dca1a09b1ca0c2e2b1b2d20de418f6398a8aed37b062917cf6d95b4a5"
265
269
  },
266
270
  {
267
271
  "path": "shared/core/multi-agent/SKILL.md",
268
272
  "sha256": "3dd44ccc00e41c09e59ad223900a391f80158bd3f69e45d67c684285bc42418d"
269
273
  },
270
- {
271
- "path": "shared/core/valven-rules/SKILL.md",
272
- "sha256": "274299b75e36991941b8402ae025967ce553878100c1a3878a955d27f5b7db9e"
273
- },
274
274
  {
275
275
  "path": "shared/external/accessibility-compliance-accessibility-audit/SKILL.md",
276
276
  "sha256": "0508b6e832b43d54c6ed5f57d42c292925b81e4df1c132ba1786860385ea2838"
@@ -455,6 +455,10 @@
455
455
  "path": "shared/external/docker-expert/SKILL.md",
456
456
  "sha256": "b3db936caa89a2c249ae64896cfb4c0a72ac45064cc081d6f99c78efe6a1f9e9"
457
457
  },
458
+ {
459
+ "path": "shared/external/e2e-testing/SKILL.md",
460
+ "sha256": "e76dac61b973a5c509a9902a15a7c18ed226d838406fd0f8b513f24b45ea4025"
461
+ },
458
462
  {
459
463
  "path": "shared/external/energykit/SKILL.md",
460
464
  "sha256": "a557acc6e324f4b841f411cfb0d9040094c8d2934aefe5cb4eecd5a2a6b0e402"
@@ -839,6 +843,10 @@
839
843
  "path": "shared/external/typescript-patterns/SKILL.md",
840
844
  "sha256": "784d294aa6ba09ff29bc7ec0f106eb512a006e79c96a01ec92d1699bfa122077"
841
845
  },
846
+ {
847
+ "path": "shared/external/valven-rules/SKILL.md",
848
+ "sha256": "4dcb5f937310523e13250780ed381b5a2677bc4a4f44b974fba2b89fe8b60173"
849
+ },
842
850
  {
843
851
  "path": "shared/external/vision-framework/SKILL.md",
844
852
  "sha256": "370fdf6eaf71833dff0e8f2d3989a849b5c42ff3198605aa818c2fd0c12c7ef7"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "skillCount": 221,
3
+ "skillCount": 223,
4
4
  "entries": [
5
5
  {
6
6
  "name": "accessibility-compliance-accessibility-audit",
@@ -519,6 +519,17 @@
519
519
  "triggerPaths": [],
520
520
  "relativePath": "shared/external/docker-expert/SKILL.md"
521
521
  },
522
+ {
523
+ "name": "e2e-testing",
524
+ "description": "End-to-end testing of a running web or mobile app through the multi-agent-toolkit MCP server: element locators instead of coordinates, expect steps that return passed / failed / blocked verdicts with codes, secrets by secret_ref, failure traces, saved and replayed step sequences, parallel web sessio",
525
+ "platform": null,
526
+ "group": "external",
527
+ "plugin": "ai-common-toolkit",
528
+ "invokeAs": "ai-common-toolkit:e2e-testing",
529
+ "triggerKeywords": [],
530
+ "triggerPaths": [],
531
+ "relativePath": "shared/external/e2e-testing/SKILL.md"
532
+ },
522
533
  {
523
534
  "name": "energykit",
524
535
  "description": "EnergyKit on iOS 26: grid electricity forecasts via ElectricityGuidance, EnergyVenue, EV and HVAC load events, ElectricityInsightService history. Use when building smart home, EV charging, HVAC scheduling or energy dashboard features that shift home power use to cleaner or cheaper grid periods.",
@@ -1025,6 +1036,17 @@
1025
1036
  "triggerPaths": [],
1026
1037
  "relativePath": "shared/core/multi-agent-autopilot-status/SKILL.md"
1027
1038
  },
1039
+ {
1040
+ "name": "multi-agent-bug-bash",
1041
+ "description": "Bug bash: parallel exploratory sessions by charter and persona, each with a budget; a finding is reported only when a repro test fails for its reason. Use when an app or branch needs broad bug hunting.",
1042
+ "platform": null,
1043
+ "group": "core",
1044
+ "plugin": null,
1045
+ "invokeAs": "multi-agent-bug-bash",
1046
+ "triggerKeywords": [],
1047
+ "triggerPaths": [],
1048
+ "relativePath": "shared/core/multi-agent-bug-bash/SKILL.md"
1049
+ },
1028
1050
  {
1029
1051
  "name": "multi-agent-build-optimize",
1030
1052
  "description": "Wrapper that dispatches to xcode-build-orchestrator on iOS repos. Benchmarks the current Xcode build, runs compilation / project / SPM analyzers, produces a recommend-first optimization plan in .build-benchmark/, and re-benchmarks after the developer approves changes. Fails fast on non-iOS stacks. U",
@@ -2292,14 +2314,14 @@
2292
2314
  },
2293
2315
  {
2294
2316
  "name": "valven-rules",
2295
- "description": "Delivery-flow rules from the metrics Valven reports: PR size, one concern per PR, tests with source, issue keys, no WIP commits, rework, PR body, small hotfixes. Use when developing, committing or reviewing a change held to these rules, or splitting a branch into stacked PRs.",
2317
+ "description": "Delivery-flow rules from metrics Valven reports: PR size, one concern per PR, tests with source, issue keys, no WIP commits, churn, PR body, and coding, pickup and review time on Valven's bands. Use when developing, opening or reviewing a pull request held to these rules, or splitting a branch into ",
2296
2318
  "platform": null,
2297
- "group": "core",
2298
- "plugin": null,
2299
- "invokeAs": "valven-rules",
2319
+ "group": "external",
2320
+ "plugin": "ai-common-toolkit",
2321
+ "invokeAs": "ai-common-toolkit:valven-rules",
2300
2322
  "triggerKeywords": [],
2301
2323
  "triggerPaths": [],
2302
- "relativePath": "shared/core/valven-rules/SKILL.md"
2324
+ "relativePath": "shared/external/valven-rules/SKILL.md"
2303
2325
  },
2304
2326
  {
2305
2327
  "name": "vision-framework",
@@ -2,12 +2,12 @@
2
2
 
3
3
  Single source of truth for skills delivered to both Claude Code (`~/.claude/skills/`) and Copilot CLI (`~/.copilot/skills/`) by the installer.
4
4
 
5
- **Total:** 221 skills (67 core + 154 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
5
+ **Total:** 223 skills (67 core + 156 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
6
6
 
7
7
  ## Directory layout
8
8
 
9
9
  - **`core/`** - 67 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
10
- - **`external/`** - 154 iOS / Android / generic skills that ship into the stack plugins. Each names its origin in `metadata.source`: `multi-agent-pipeline` when written here, or the upstream and license a `NOTICE-*.md` in this directory covers.
10
+ - **`external/`** - 156 iOS / Android / generic skills that ship into the stack plugins. Each names its origin in `metadata.source`: `multi-agent-pipeline` when written here, or the upstream and license a `NOTICE-*.md` in this directory covers.
11
11
  - Install destinations (ADR-0009): Claude Code gets NO local copy of `external/` - it loads those skills from the `multi-agent-plugins` marketplace, namespaced (`ai-<stack>-toolkit:<name>`); only the two compliance catalogs from `core/` land in `~/.claude/skills/`. Copilot CLI and Codex CLI receive a flat copy filtered to the enabled stacks. `external/` remains the single authoring source that `build-stack-plugins.mjs` publishes from.
12
12
 
13
13
  Source layout is logical grouping only - skill discovery at runtime is unchanged.
@@ -19,7 +19,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
19
19
  - [Android / Kotlin](#android-kotlin) - 16
20
20
  - [Web](#web) - 10
21
21
  - [Backend / API](#backend-api) - 11
22
- - [Cross-cutting](#cross-cutting) - 28
22
+ - [Cross-cutting](#cross-cutting) - 30
23
23
 
24
24
  ## Pipeline Orchestration
25
25
 
@@ -35,6 +35,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
35
35
  | [`multi-agent-autopilot-off`](./core/multi-agent-autopilot-off/) | `core` | Turn off continuous mode on this machine: the schedule is removed, work already running finishes, the repo selection is kept. Use when the m |
36
36
  | [`multi-agent-autopilot-on`](./core/multi-agent-autopilot-on/) | `core` | Turn on continuous mode on THIS machine: pick the repos it watches; labelled GitHub issues and assigned+labelled Jira items then run in a wo |
37
37
  | [`multi-agent-autopilot-status`](./core/multi-agent-autopilot-status/) | `core` | Show what continuous mode is doing here: what runs at which phase, what is queued, what waits for an answer, what PRs it opened today. Use w |
38
+ | [`multi-agent-bug-bash`](./core/multi-agent-bug-bash/) | `core` | Bug bash: parallel exploratory sessions by charter and persona, each with a budget; a finding is reported only when a repro test fails for i |
38
39
  | [`multi-agent-build-optimize`](./core/multi-agent-build-optimize/) | `core` | Wrapper that dispatches to xcode-build-orchestrator on iOS repos. Benchmarks the current Xcode build, runs compilation / project / SPM analy |
39
40
  | [`multi-agent-channels`](./core/multi-agent-channels/) | `core` | Multi-channel reporter - Jira/Confluence/Wiki/PR description. Multi-select channels + content, humanizer pass, reviewer-preserving Bitbuck |
40
41
  | [`multi-agent-complaint-analysis`](./core/multi-agent-complaint-analysis/) | `core` | Customer-complaint triage. Ingests complaints (paste, csv/xlsx/txt/json file, Jira issue, Confluence URL), fetches Graylog evidence per trx/ |
@@ -91,7 +92,6 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
91
92
  | [`multi-agent-testflight-validation`](./core/multi-agent-testflight-validation/) | `core` | iOS-pinned alias of store-ready: same three gates on an iOS build, one implementation. Validates a TestFlight / App Store package, never upl |
92
93
  | [`multi-agent-uninstall`](./core/multi-agent-uninstall/) | `core` | Uninstall the pipeline from Claude Code + Copilot CLI. Keychain access tokens are always left untouched; --all-data also clears pipeline set |
93
94
  | [`multi-agent-update`](./core/multi-agent-update/) | `core` | Update the pipeline to the latest version: git pull, install, migrate. Use when the installed pipeline is behind and should be brought to th |
94
- | [`valven-rules`](./core/valven-rules/) | `core` | Delivery-flow rules from the metrics Valven reports: PR size, one concern per PR, tests with source, issue keys, no WIP commits, rework, PR |
95
95
 
96
96
  ## iOS / Apple Ecosystem
97
97
 
@@ -255,6 +255,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
255
255
  | [`cryptokit`](./external/cryptokit/) | `external` | CryptoKit in Swift: SHA-2, SHA-3, HMAC, AES-GCM, ChaChaPoly, P256, P384, P521, Curve25519 and ML-DSA signatures, ECDH with HKDF, HPKE, ML-KE |
256
256
  | [`debugging-instruments`](./external/debugging-instruments/) | `external` | Xcode debugging and profiling: LLDB, Memory Graph Debugger, Instruments, crashes, leaks and retain cycles, main-thread hangs, slow rendering |
257
257
  | [`debugging-strategies`](./external/debugging-strategies/) | `external` | Language-neutral debugging method: reproduce, minimise, falsifiable hypotheses, git bisect, instrumentation, production traces, concurrency |
258
+ | [`e2e-testing`](./external/e2e-testing/) | `external` | \| |
258
259
  | [`evidence-github`](./external/evidence-github/) | `external` | Search GitHub for facts an analysis can cite - a known issue in a dependency, the PR that fixed it, what a release actually changed, prior a |
259
260
  | [`evidence-registry`](./external/evidence-registry/) | `external` | Check a package's real version, deprecation status and release notes on npm, PyPI, Maven Central or Swift Package Index. Use when a spec or |
260
261
  | [`github-actions-templates`](./external/github-actions-templates/) | `external` | Authoring GitHub Actions workflows: triggers, matrices, caching, concurrency, least-privilege permissions, SHA pinning, OIDC, reusable workf |
@@ -266,6 +267,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
266
267
  | [`signal-community`](./external/signal-community/) | `external` | Check whether other people hit the same problem, via Stack Overflow and Hacker News (keyless) plus opt-in Reddit and X. Use when a bug repor |
267
268
  | [`skill-creator`](./external/skill-creator/) | `external` | House guide for authoring plugin skills: creating a new skill, trimming or splitting a bloated one, writing or sharpening a description, and |
268
269
  | [`spm-build-analysis`](./external/spm-build-analysis/) | `external` | Analyze Swift Package Manager dependencies, package plugins, module variants, and CI-oriented build overhead that slow Xcode builds. Use whe |
270
+ | [`valven-rules`](./external/valven-rules/) | `external` | Delivery-flow rules from metrics Valven reports: PR size, one concern per PR, tests with source, issue keys, no WIP commits, churn, PR body, |
269
271
  | [`xcode-build-benchmark`](./external/xcode-build-benchmark/) | `external` | Benchmark Xcode clean and incremental builds with repeatable inputs, timing summaries, and timestamped `.build-benchmark/` artifacts. Use wh |
270
272
  | [`xcode-build-fixer`](./external/xcode-build-fixer/) | `external` | Apply approved Xcode build optimization changes following best practices, then re-benchmark to verify improvement. Use when a developer has |
271
273
  | [`xcode-build-orchestrator`](./external/xcode-build-orchestrator/) | `external` | Orchestrate Xcode build optimization by benchmarking first, running the specialist analysis skills, prioritizing findings, requesting explic |