@mmerterden/multi-agent-pipeline 14.2.2 → 15.0.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 (122) hide show
  1. package/CHANGELOG.md +76 -6
  2. package/README.md +15 -8
  3. package/README.tr.md +15 -8
  4. package/docs/FIGMA_PIPELINE.md +3 -3
  5. package/docs/adr/0006-skills-core-external-split.md +1 -1
  6. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  7. package/docs/adr/README.md +1 -0
  8. package/docs/architecture.md +7 -7
  9. package/docs/ecosystem.md +28 -28
  10. package/docs/features.md +5 -5
  11. package/index.js +2 -0
  12. package/install/_codex-agents.mjs +11 -2
  13. package/install/_common.mjs +65 -1
  14. package/install/_dev-only-files.mjs +0 -1
  15. package/install/_platform-filter.mjs +73 -7
  16. package/install/_plugin-skills.mjs +19 -8
  17. package/install/claude.mjs +144 -59
  18. package/install/codex.mjs +28 -3
  19. package/install/copilot.mjs +36 -11
  20. package/install/index.mjs +6 -2
  21. package/install/templates/codex-instructions.md +1 -1
  22. package/install/templates/copilot-instructions.md +3 -3
  23. package/package.json +1 -2
  24. package/pipeline/commands/multi-agent/SKILL.md +2 -0
  25. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
  26. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  27. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  28. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  29. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  30. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
  32. package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
  33. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
  34. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  35. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  36. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  37. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
  38. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  39. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
  41. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  42. package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
  43. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  44. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  45. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  47. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  48. package/pipeline/lib/issue-fetcher.sh +1 -1
  49. package/pipeline/lib/parse-complaints.sh +306 -0
  50. package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
  51. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  52. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  53. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  54. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  55. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  56. package/pipeline/multi-agent-refs/generate-issue.md +1 -1
  57. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  58. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  59. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  60. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  61. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  62. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  63. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  64. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  65. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  66. package/pipeline/preferences-template.json +13 -5
  67. package/pipeline/rules/figma-pipeline.md +2 -2
  68. package/pipeline/schemas/agent-state.schema.json +1 -1
  69. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  70. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  71. package/pipeline/schemas/prefs.schema.json +276 -66
  72. package/pipeline/schemas/token-budget.json +2 -2
  73. package/pipeline/scripts/_stack-routing.mjs +79 -0
  74. package/pipeline/scripts/audit-log-rotate.sh +4 -1
  75. package/pipeline/scripts/build-skills-index.mjs +11 -0
  76. package/pipeline/scripts/build-stack-plugins.mjs +28 -60
  77. package/pipeline/scripts/check-derived-drift.mjs +52 -28
  78. package/pipeline/scripts/gc-worktrees.sh +4 -1
  79. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  80. package/pipeline/scripts/match-skills.mjs +8 -2
  81. package/pipeline/scripts/migrate-prefs.mjs +28 -20
  82. package/pipeline/scripts/phase-tracker.sh +13 -5
  83. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  84. package/pipeline/scripts/run-aggregator.mjs +7 -2
  85. package/pipeline/scripts/scan-agent-config.sh +1 -1
  86. package/pipeline/scripts/skill-conformance.mjs +165 -30
  87. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  88. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  89. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  90. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  91. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  92. package/pipeline/scripts/uninstall.mjs +158 -11
  93. package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
  94. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  95. package/pipeline/skills/.skill-manifest.json +156 -108
  96. package/pipeline/skills/.skills-index.json +449 -12
  97. package/pipeline/skills/shared/README.md +14 -10
  98. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  99. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  100. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  101. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
  102. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
  103. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
  104. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
  105. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  106. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  107. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
  108. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
  109. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  110. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  111. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  112. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  113. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  114. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  115. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  116. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  117. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  118. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  119. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
  121. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
  122. package/pipeline/skills/skills-index.md +7 -4
@@ -97,7 +97,9 @@ function resolveTrackerCandidates() {
97
97
  if (!HOME) die("neither HOME nor USERPROFILE is set - pass --task-dir");
98
98
  const taskId = String(flags["task-id"]);
99
99
  if (flags.project) {
100
- candidates.push(join(HOME, ".claude", "logs", "multi-agent", flags.project, taskId, "tracker-state.json"));
100
+ candidates.push(
101
+ join(HOME, ".claude", "logs", "multi-agent", flags.project, taskId, "tracker-state.json"),
102
+ );
101
103
  }
102
104
  candidates.push(join(HOME, ".claude", "logs", "multi-agent", taskId, "tracker-state.json"));
103
105
  }
@@ -209,7 +211,10 @@ let unknownModelHits = 0;
209
211
  // rest of this file can treat it as a plain array.
210
212
  const phaseList = Array.isArray(tracker.phases)
211
213
  ? tracker.phases
212
- : Object.entries(tracker.phases || {}).map(([key, value]) => ({ ...value, id: value?.id ?? key }));
214
+ : Object.entries(tracker.phases || {}).map(([key, value]) => ({
215
+ ...value,
216
+ id: value?.id ?? key,
217
+ }));
213
218
 
214
219
  for (const phase of phaseList) {
215
220
  const tIn = phase.tokens_in || 0;
@@ -31,7 +31,7 @@ add "$ROOT/install/templates/claude-hooks.json"
31
31
  add "$ROOT/install/templates/copilot-instructions.md"
32
32
  add "$ROOT/pipeline/preferences-template.json"
33
33
  for f in "$ROOT"/pipeline/agents/*.md; do add "$f"; done
34
- # (figma component skills + their scripts now live in the ai-*-engineering-toolkit
34
+ # (figma component skills + their scripts now live in the ai-*-toolkit
35
35
  # marketplace plugin, not in this repo, so there is nothing figma to scan here.)
36
36
  add "$ROOT/pipeline/scripts/agent-guard.sh"
37
37
  add "$ROOT/pipeline/scripts/pre-commit-check.sh"
@@ -43,6 +43,7 @@
43
43
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
44
44
  import { dirname, join, resolve, relative, basename, extname, sep } from "node:path";
45
45
  import { fileURLToPath } from "node:url";
46
+ import { homedir } from "node:os";
46
47
 
47
48
  const here = dirname(fileURLToPath(import.meta.url));
48
49
 
@@ -64,10 +65,74 @@ function skillsRootCandidates() {
64
65
  return [
65
66
  join(installRoot, "skills"), // ~/.claude/skills, ~/.copilot/skills
66
67
  join(installRoot, "multi-agent-refs", "skills"), // ~/.codex/multi-agent-refs/skills
68
+ ...pluginSkillRoots(), // marketplace stack plugins (Claude Code's only stack-skill source)
67
69
  join(repoRoot, "pipeline", "skills"), // checkout
68
70
  ];
69
71
  }
70
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // Marketplace plugin roots.
75
+ //
76
+ // On Claude Code the stack skills no longer land in ~/.claude/skills - they are
77
+ // loaded from the multi-agent-plugins marketplace. The registries this gate
78
+ // depends on (`ios-coding-standard`, `ios-module-structure`) live in the iOS
79
+ // plugin's knowledge/ tree, so without these roots the registry set silently
80
+ // collapses to the two local compliance catalogs and Phase 4 reads green over
81
+ // criteria that were never loaded. Local checkout is preferred over the plugin
82
+ // cache for the same reason install/_plugin-skills.mjs prefers it: the checkout
83
+ // is what /multi-agent:sync rebuilds, the cache only advances on marketplace
84
+ // refresh. Cache dirs are version-named; newest wins.
85
+ // ---------------------------------------------------------------------------
86
+ const PLUGIN_MARKETPLACE = "multi-agent-plugins";
87
+ const STACK_PLUGIN_NAMES = [
88
+ "ai-ios-toolkit",
89
+ "ai-android-toolkit",
90
+ "ai-frontend-toolkit",
91
+ "ai-backend-toolkit",
92
+ "ai-common-toolkit",
93
+ ];
94
+
95
+ function semverCompare(a, b) {
96
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
97
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
98
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
99
+ const d = (pa[i] || 0) - (pb[i] || 0);
100
+ if (d !== 0) return d;
101
+ }
102
+ return a.localeCompare(b);
103
+ }
104
+
105
+ function pluginSkillRoots() {
106
+ const home = homedir();
107
+ const roots = [];
108
+ for (const name of STACK_PLUGIN_NAMES) {
109
+ const checkout = join(home, PLUGIN_MARKETPLACE, "plugins", name, "skills");
110
+ if (existsSync(checkout)) {
111
+ roots.push(checkout);
112
+ continue;
113
+ }
114
+ const cacheRoot = join(home, ".claude", "plugins", "cache", PLUGIN_MARKETPLACE, name);
115
+ if (!existsSync(cacheRoot)) continue;
116
+ const versions = readdirSafe(cacheRoot)
117
+ .filter((v) => {
118
+ try {
119
+ return statSync(join(cacheRoot, v)).isDirectory();
120
+ } catch {
121
+ return false;
122
+ }
123
+ })
124
+ .sort(semverCompare);
125
+ for (const v of versions.reverse()) {
126
+ const p = join(cacheRoot, v, "skills");
127
+ if (existsSync(p)) {
128
+ roots.push(p);
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ return roots;
134
+ }
135
+
71
136
  /**
72
137
  * Resolve the roots to search, and report every candidate that was PROBED - not
73
138
  * only the ones that existed.
@@ -496,28 +561,64 @@ function auditExceptions(addedByFile, knownIds, today, markerToken) {
496
561
  const reason = rest.replace(/(\d{4}-\d{2}-\d{2})/, "").trim();
497
562
 
498
563
  if (!id) {
499
- findings.push(f(file, line, "blocking", null, "Exception marker names no rule ID.",
500
- "Name the rule the exception applies to: standard:exception(<RULE-ID>) <reason> <expiry>."));
564
+ findings.push(
565
+ f(
566
+ file,
567
+ line,
568
+ "blocking",
569
+ null,
570
+ "Exception marker names no rule ID.",
571
+ "Name the rule the exception applies to: standard:exception(<RULE-ID>) <reason> <expiry>.",
572
+ ),
573
+ );
501
574
  continue;
502
575
  }
503
576
  if (knownIds.size > 0 && !knownIds.has(id)) {
504
- findings.push(f(file, line, "blocking", id,
505
- `Exception cites '${id}', which is not in any registry in scope for this diff.`,
506
- "Use a real rule ID, or drop the marker. An exception against an unknown rule suppresses nothing and hides intent."));
577
+ findings.push(
578
+ f(
579
+ file,
580
+ line,
581
+ "blocking",
582
+ id,
583
+ `Exception cites '${id}', which is not in any registry in scope for this diff.`,
584
+ "Use a real rule ID, or drop the marker. An exception against an unknown rule suppresses nothing and hides intent.",
585
+ ),
586
+ );
507
587
  }
508
588
  if (!dateMatch) {
509
- findings.push(f(file, line, "important", id,
510
- `Exception for '${id}' has no expiry date.`,
511
- "Add an expiry: standard:exception(<RULE-ID>) <reason> YYYY-MM-DD. An exception with no expiry is a permanent silent carve-out."));
589
+ findings.push(
590
+ f(
591
+ file,
592
+ line,
593
+ "important",
594
+ id,
595
+ `Exception for '${id}' has no expiry date.`,
596
+ "Add an expiry: standard:exception(<RULE-ID>) <reason> YYYY-MM-DD. An exception with no expiry is a permanent silent carve-out.",
597
+ ),
598
+ );
512
599
  } else if (dateMatch[1] < today) {
513
- findings.push(f(file, line, "blocking", id,
514
- `Exception for '${id}' expired on ${dateMatch[1]}.`,
515
- "Fix the underlying violation, or renew the exception with a new expiry and a current reason."));
600
+ findings.push(
601
+ f(
602
+ file,
603
+ line,
604
+ "blocking",
605
+ id,
606
+ `Exception for '${id}' expired on ${dateMatch[1]}.`,
607
+ "Fix the underlying violation, or renew the exception with a new expiry and a current reason.",
608
+ ),
609
+ );
516
610
  }
517
611
  if (!reason) {
518
- findings.push(f(file, line, "important", id,
519
- `Exception for '${id}' gives no reason.`,
520
- "State why the rule does not apply here. An unexplained exception cannot be reviewed, only trusted."));
612
+ findings.push(
613
+ f(
614
+ file,
615
+ line,
616
+ "important",
617
+ id,
618
+ `Exception for '${id}' gives no reason.`,
619
+ "State why the rule does not apply here. An unexplained exception cannot be reviewed, only trusted.",
620
+ ),
621
+ );
521
622
  }
522
623
  }
523
624
  }
@@ -614,7 +715,8 @@ function parseArgs(argv) {
614
715
  else if (a === "--repo") o.repo = argv[++i];
615
716
  else if (a === "--out") o.out = argv[++i];
616
717
  else if (a === "--skills-root") o.skillsRoot = argv[++i];
617
- else if (a === "--today") o.today = argv[++i]; // tests pin the clock
718
+ else if (a === "--today")
719
+ o.today = argv[++i]; // tests pin the clock
618
720
  else {
619
721
  console.error(`unknown argument: ${a}`);
620
722
  process.exit(1);
@@ -692,7 +794,11 @@ const unparseable = [...escapedDeclarations];
692
794
 
693
795
  for (const d of discovered) {
694
796
  if (!existsSync(d.rulesPath)) {
695
- unparseable.push({ name: d.name, path: d.rulesPath, reason: "declared standards-registry file does not exist" });
797
+ unparseable.push({
798
+ name: d.name,
799
+ path: d.rulesPath,
800
+ reason: "declared standards-registry file does not exist",
801
+ });
696
802
  continue;
697
803
  }
698
804
  let loaded;
@@ -703,7 +809,11 @@ for (const d of discovered) {
703
809
  continue;
704
810
  }
705
811
  if (loaded.rules.length === 0) {
706
- unparseable.push({ name: d.name, path: d.rulesPath, reason: "no rules extracted - rules: block missing or unexpected indentation" });
812
+ unparseable.push({
813
+ name: d.name,
814
+ path: d.rulesPath,
815
+ reason: "no rules extracted - rules: block missing or unexpected indentation",
816
+ });
707
817
  continue;
708
818
  }
709
819
  registries.push({ ...d, ...loaded });
@@ -716,9 +826,10 @@ const perRegistry = [];
716
826
  for (const reg of registries) {
717
827
  const scope = reg.meta.scope;
718
828
  const regLanguages = scope?.languages ?? [];
719
- const langHit = regLanguages.length === 0
720
- ? Object.keys(languages)
721
- : regLanguages.filter((l) => Object.prototype.hasOwnProperty.call(languages, l));
829
+ const langHit =
830
+ regLanguages.length === 0
831
+ ? Object.keys(languages)
832
+ : regLanguages.filter((l) => Object.prototype.hasOwnProperty.call(languages, l));
722
833
 
723
834
  // Files this registry may speak about at all.
724
835
  const inScopeFiles = changedFiles.filter((file) => {
@@ -747,7 +858,9 @@ for (const reg of registries) {
747
858
  ? inScopeFiles.filter((f2) => matchesAnyGlob(f2, rule.scope.paths))
748
859
  : inScopeFiles;
749
860
  if (files.length === 0) {
750
- const why = rule.scopeReason ? `out-of-scope: ${rule.scopeReason}` : "out-of-scope: no changed file matches the rule's paths";
861
+ const why = rule.scopeReason
862
+ ? `out-of-scope: ${rule.scopeReason}`
863
+ : "out-of-scope: no changed file matches the rule's paths";
751
864
  droppedReasons[why] = (droppedReasons[why] ?? 0) + 1;
752
865
  continue;
753
866
  }
@@ -776,9 +889,16 @@ for (const reg of registries) {
776
889
  languagesMatched: langHit,
777
890
  inScopeFileCount: inScopeFiles.length,
778
891
  notCovered: reg.meta.notCovered,
779
- loadedAs: unscoped ? "reference-only" : inScopeFiles.length ? "rule-checklist" : "not-applicable",
892
+ loadedAs: unscoped
893
+ ? "reference-only"
894
+ : inScopeFiles.length
895
+ ? "rule-checklist"
896
+ : "not-applicable",
780
897
  ...(unscoped
781
- ? { referenceOnlyReason: "registry declares no scope; applying its rules to unknown languages would manufacture findings" }
898
+ ? {
899
+ referenceOnlyReason:
900
+ "registry declares no scope; applying its rules to unknown languages would manufacture findings",
901
+ }
782
902
  : {}),
783
903
  toolchains: detectToolchains(repoRoot, reg.rules),
784
904
  exceptionMarker: reg.meta.exceptionMarker,
@@ -794,7 +914,9 @@ for (const [lang, files] of Object.entries(languages)) {
794
914
  const covered = files.filter((f2) => coveredFiles.has(f2));
795
915
  if (covered.length === 0) {
796
916
  const claims = perRegistry
797
- .flatMap((r) => Object.entries(r.notCovered ?? {}).map(([k, v]) => ({ registry: r.name, key: k, note: v })))
917
+ .flatMap((r) =>
918
+ Object.entries(r.notCovered ?? {}).map(([k, v]) => ({ registry: r.name, key: k, note: v })),
919
+ )
798
920
  .filter((c) => c.key === lang || lang.startsWith(c.key));
799
921
  gaps.push({
800
922
  language: lang,
@@ -803,7 +925,8 @@ for (const [lang, files] of Object.entries(languages)) {
803
925
  reason: claims.length
804
926
  ? `no registry in scope covers ${lang}: ${claims.map((c) => `${c.registry} declares '${c.key}' not covered`).join("; ")}`
805
927
  : `no registry in scope declares ${lang}`,
806
- consequence: "these files are reviewed on general criteria only - no rule IDs apply to them",
928
+ consequence:
929
+ "these files are reviewed on general criteria only - no rule IDs apply to them",
807
930
  });
808
931
  }
809
932
  }
@@ -815,7 +938,7 @@ const { guides, dropped: guidesDropped } = collectModuleGuides(repoRoot, changed
815
938
  // knownIds spans EVERY discovered registry, not just the in-scope ones. Narrowing
816
939
  // it to the selected set would report a valid rule as unknown whenever the
817
940
  // exception sits in a file that rule does not cover, which is a false accusation
818
- // in the one place the author is trying to be explicit. The narrower question -
941
+ // in the one place the author is trying to be explicit. The narrower question -
819
942
  // "is this exception citing a rule that applies here?" - is answered separately
820
943
  // below, so a mis-scoped exception is still reported, just not as a typo.
821
944
  const knownIds = new Set(registries.flatMap((r) => r.rules.map((x) => x.id)));
@@ -872,7 +995,9 @@ for (const reg of perRegistry) {
872
995
  const resolvedNames = new Set(perRegistry.map((r) => r.name));
873
996
  const declaredNames = ledger.entries.map((e) => e?.skill).filter(Boolean);
874
997
  const declaredNotResolved = [...new Set(declaredNames)].filter((n) => !resolvedNames.has(n));
875
- const ledgerMissingTargets = ledger.entries.filter((e) => !Array.isArray(e?.targetFiles) || e.targetFiles.length === 0).length;
998
+ const ledgerMissingTargets = ledger.entries.filter(
999
+ (e) => !Array.isArray(e?.targetFiles) || e.targetFiles.length === 0,
1000
+ ).length;
876
1001
 
877
1002
  // Entries a stack toolkit's own index chose (Phase 3 step 9 stamps `routedBy`).
878
1003
  // Surfaced separately because "the project's toolkit says this skill governs this
@@ -940,10 +1065,20 @@ if (opts.json) {
940
1065
  process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
941
1066
  } else {
942
1067
  console.log(`criteria-manifest: ${outPath}`);
943
- console.log(` languages: ${Object.entries(manifest.languages).map(([k, v]) => `${k}:${v}`).join(" ") || "(none)"}`);
944
- console.log(` registries: ${perRegistry.length} discovered, ${perRegistry.filter((r) => r.loadedAs === "rule-checklist").length} as rule-checklist`);
1068
+ console.log(
1069
+ ` languages: ${
1070
+ Object.entries(manifest.languages)
1071
+ .map(([k, v]) => `${k}:${v}`)
1072
+ .join(" ") || "(none)"
1073
+ }`,
1074
+ );
1075
+ console.log(
1076
+ ` registries: ${perRegistry.length} discovered, ${perRegistry.filter((r) => r.loadedAs === "rule-checklist").length} as rule-checklist`,
1077
+ );
945
1078
  console.log(` selected rules: ${selected.length}`);
946
- console.log(` coverage: ${coveredFiles.size}/${changedFiles.length} changed files carry rule criteria`);
1079
+ console.log(
1080
+ ` coverage: ${coveredFiles.size}/${changedFiles.length} changed files carry rule criteria`,
1081
+ );
947
1082
  for (const g of gaps) console.log(` GAP ${g.language} (${g.fileCount} file(s)): ${g.reason}`);
948
1083
  console.log(` ledger: ${manifest.ledger.source} (self-report: ${ledger.status})`);
949
1084
  console.log(` findings: ${findings.length}`);
@@ -152,7 +152,7 @@ fi
152
152
  # ──────────────────────────────────────────────────────────────────────────
153
153
  echo ""
154
154
  # Figma/component skill trees no longer ship in the pipeline (v11.2.0+): they
155
- # live in the marketplace component plugins (ai-ios-engineering-toolkit /
155
+ # live in the marketplace component plugins (ai-ios-toolkit /
156
156
  # multi-agent-plugins), which each CLI loads through its own plugin channel.
157
157
  # The pipeline therefore has no figma tree to deploy or parity-check here.
158
158
  echo "→ 7. Agent personas deployed to both CLIs"
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "android",
4
+ "description": "Android / Jetpack Compose / Kotlin test gap rules. Sources in *.kt; tests under app/src/test/ or app/src/androidTest/.",
5
+ "sourceExtensions": [".kt", ".java"],
6
+ "testPathSuffixes": ["Test.kt", "Tests.kt", "Test.java"],
7
+ "testPathContains": ["/src/test/", "/src/androidTest/", "/test/", "/tests/"],
8
+ "excludePathGlobs": ["build/", ".gradle/", "generated/", "**/BuildConfig.java", "**/R.java"],
9
+ "publicApiPatterns": [
10
+ { "id": "open_class", "regex": "\\bopen\\s+class\\s+([A-Z][A-Za-z0-9_]*)" },
11
+ { "id": "open_fun", "regex": "\\bopen\\s+fun\\s+([a-z_][A-Za-z0-9_]*)" },
12
+ { "id": "public_fun", "regex": "^\\s*(?:public\\s+)?fun\\s+([a-z_][A-Za-z0-9_]*)" },
13
+ { "id": "public_class", "regex": "^\\s*(?:public\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)" },
14
+ { "id": "jvmstatic", "regex": "@JvmStatic\\b" },
15
+ { "id": "interface", "regex": "^\\s*(?:public\\s+)?interface\\s+([A-Z][A-Za-z0-9_]*)" }
16
+ ],
17
+ "composablePatterns": [
18
+ {
19
+ "id": "composable_fun",
20
+ "regex": "@Composable\\s+(?:public\\s+|internal\\s+)?fun\\s+([A-Z][A-Za-z0-9_]*)"
21
+ }
22
+ ],
23
+ "expectedTestKindsForComposable": ["compose-ui-test"],
24
+ "hint": "Cover @Composable functions with createComposeRule() + onNodeWithText/Tag/ContentDescription assertions; ViewModels with runTest + Turbine."
25
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "ios",
4
+ "description": "iOS / SwiftUI / UIKit test gap rules. Source files in *.swift; tests in *Tests.swift. Snapshot/ViewInspector tests are also recognised when the source is a SwiftUI View.",
5
+ "sourceExtensions": [".swift"],
6
+ "testPathSuffixes": ["Tests.swift", "Test.swift"],
7
+ "testPathContains": ["/Tests/", "/UnitTests/", "/SnapshotTests/"],
8
+ "excludePathGlobs": [
9
+ "Pods/",
10
+ "Carthage/",
11
+ ".build/",
12
+ "DerivedData/",
13
+ "Generated/",
14
+ "Sourcery/",
15
+ "_Generated.swift",
16
+ "Tests/Fixtures/"
17
+ ],
18
+ "publicApiPatterns": [
19
+ { "id": "public_func", "regex": "\\bpublic\\s+func\\s+([A-Za-z_][A-Za-z0-9_]*)" },
20
+ { "id": "public_class", "regex": "\\bpublic\\s+(?:final\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)" },
21
+ { "id": "public_struct", "regex": "\\bpublic\\s+struct\\s+([A-Z][A-Za-z0-9_]*)" },
22
+ { "id": "public_enum", "regex": "\\bpublic\\s+enum\\s+([A-Z][A-Za-z0-9_]*)" },
23
+ { "id": "public_proto", "regex": "\\bpublic\\s+protocol\\s+([A-Z][A-Za-z0-9_]*)" },
24
+ { "id": "public_init", "regex": "\\bpublic\\s+init\\b" },
25
+ { "id": "objc_export", "regex": "@objc(?:Members)?\\s+(?:public\\s+)?(?:func|class|var)" }
26
+ ],
27
+ "swiftUiViewPatterns": [
28
+ { "id": "view_struct", "regex": "struct\\s+([A-Z][A-Za-z0-9_]*)\\s*:\\s*View\\b" },
29
+ { "id": "config_struct", "regex": "struct\\s+([A-Z][A-Za-z0-9_]*Configuration)\\b" }
30
+ ],
31
+ "expectedTestKindsForView": ["snapshot", "viewinspector", "unit"],
32
+ "snapshotPathHints": ["SnapshotTests", "__Snapshots__"],
33
+ "hint": "Cover with a 3-layer test (structural ViewInspector + snapshot + unit) per swiftui-qa.md."
34
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "node",
4
+ "description": "Node.js / TypeScript test gap rules. Sources in *.ts/*.tsx/*.js/*.mjs; tests in *.test.* / *.spec.* / __tests__/.",
5
+ "sourceExtensions": [".ts", ".tsx", ".js", ".mjs", ".cjs"],
6
+ "testPathSuffixes": [
7
+ ".test.ts",
8
+ ".test.tsx",
9
+ ".test.js",
10
+ ".test.mjs",
11
+ ".spec.ts",
12
+ ".spec.tsx",
13
+ ".spec.js",
14
+ ".spec.mjs"
15
+ ],
16
+ "testPathContains": ["/__tests__/", "/test/", "/tests/"],
17
+ "excludePathGlobs": ["node_modules/", "dist/", "build/", ".next/", "coverage/", "*.d.ts"],
18
+ "publicApiPatterns": [
19
+ {
20
+ "id": "named_export",
21
+ "regex": "^export\\s+(?:async\\s+)?(?:function|class|const|let|interface|type|enum)\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
22
+ },
23
+ {
24
+ "id": "default_export",
25
+ "regex": "^export\\s+default\\s+(?:async\\s+)?(?:function|class)\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
26
+ }
27
+ ],
28
+ "hint": "Cover named exports with vitest/jest test('<name>', ...). React components: React Testing Library (render + screen + userEvent)."
29
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "python",
4
+ "description": "Python test gap rules. Sources in *.py; tests in test_*.py / *_test.py / tests/.",
5
+ "sourceExtensions": [".py"],
6
+ "testPathSuffixes": ["_test.py"],
7
+ "testPathPrefixes": ["test_"],
8
+ "testPathContains": ["/tests/", "/test/"],
9
+ "excludePathGlobs": [
10
+ "__pycache__/",
11
+ ".venv/",
12
+ "venv/",
13
+ ".tox/",
14
+ "build/",
15
+ "dist/",
16
+ "*.egg-info/",
17
+ "**/migrations/**"
18
+ ],
19
+ "publicApiPatterns": [
20
+ { "id": "top_def", "regex": "^def\\s+([a-z_][a-z0-9_]*)" },
21
+ { "id": "top_class", "regex": "^class\\s+([A-Z][A-Za-z0-9_]*)" },
22
+ { "id": "async_def", "regex": "^async\\s+def\\s+([a-z_][a-z0-9_]*)" }
23
+ ],
24
+ "hint": "Cover with pytest test_<symbol> style. Async with pytest-asyncio. Avoid name collision with private helpers (prefix _)."
25
+ }