@mmerterden/multi-agent-pipeline 19.0.0 → 19.1.1

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 (61) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +3 -3
  3. package/docs/ecosystem.md +12 -4
  4. package/docs/facts.json +20 -4
  5. package/docs/features.md +1 -1
  6. package/docs/recovery-guide.md +8 -8
  7. package/manifest.json +64 -62
  8. package/package.json +1 -1
  9. package/pipeline/agents/dev-critic.md +4 -4
  10. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  11. package/pipeline/commands/multi-agent/analysis/SKILL.md +6 -6
  12. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  13. package/pipeline/commands/multi-agent/resume-local/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  15. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +1 -1
  16. package/pipeline/lib/model-dispatch.sh +140 -0
  17. package/pipeline/lib/outbound-gate.mjs +14 -0
  18. package/pipeline/multi-agent-refs/_dev-context.md +5 -5
  19. package/pipeline/multi-agent-refs/analysis/evidence.md +2 -2
  20. package/pipeline/multi-agent-refs/analysis/intake.md +6 -6
  21. package/pipeline/multi-agent-refs/analysis/locked.md +27 -0
  22. package/pipeline/multi-agent-refs/analysis/redesign.md +1 -1
  23. package/pipeline/multi-agent-refs/analysis/render.md +9 -9
  24. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  25. package/pipeline/multi-agent-refs/analysis/review.md +2 -2
  26. package/pipeline/multi-agent-refs/analysis/synthesis.md +2 -2
  27. package/pipeline/multi-agent-refs/analysis-template-corporate.md +9 -9
  28. package/pipeline/multi-agent-refs/analysis-template.md +19 -19
  29. package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
  30. package/pipeline/multi-agent-refs/conventions-defaults.md +2 -2
  31. package/pipeline/multi-agent-refs/features/analysis-jira.md +1 -1
  32. package/pipeline/multi-agent-refs/features/doctor.md +1 -1
  33. package/pipeline/multi-agent-refs/features/model-fallback.md +36 -0
  34. package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
  35. package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
  36. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
  37. package/pipeline/multi-agent-refs/phases/phase-3-review.md +2 -2
  38. package/pipeline/preferences-template.json +5 -0
  39. package/pipeline/rules/figma-pipeline.md +8 -8
  40. package/pipeline/schemas/analysis-output.schema.json +1 -1
  41. package/pipeline/schemas/analysis-spec.schema.json +2 -2
  42. package/pipeline/schemas/figma-project-config.schema.json +1 -1
  43. package/pipeline/schemas/prefs.schema.json +2 -2
  44. package/pipeline/schemas/secret-patterns.json +124 -0
  45. package/pipeline/scripts/build-references.mjs +2 -2
  46. package/pipeline/scripts/bulk-read.sh +10 -1
  47. package/pipeline/scripts/cost-table.json +8 -1
  48. package/pipeline/scripts/doctor.mjs +1 -1
  49. package/pipeline/scripts/gen-facts.mjs +112 -7
  50. package/pipeline/scripts/phase-tracker.sh +5 -5
  51. package/pipeline/scripts/pre-commit-check.sh +30 -1
  52. package/pipeline/scripts/scan-skills.sh +26 -0
  53. package/pipeline/scripts/validate-analysis-doc.mjs +201 -26
  54. package/pipeline/scripts/verify-citations.mjs +1 -1
  55. package/pipeline/scripts/write-state.mjs +32 -0
  56. package/pipeline/skills/.skill-manifest.json +5 -5
  57. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +6 -6
  58. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +6 -6
  59. package/pipeline/skills/shared/core/multi-agent/SKILL.md +14 -13
  60. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +1 -1
  61. package/pipeline/skills/shared/external/signal-community/SKILL.md +8 -1
@@ -27,7 +27,7 @@
27
27
  // - Manual scenarios (Section 15.7): every MT- row carries a BR- id.
28
28
  // - Variant coverage (Section 6.X): every axis row states the full axis and
29
29
  // the subset this screen uses; half a row cannot be audited.
30
- // - Traceability (Full mode only, Locked 31): every BR-<slug>-NN id defined in
30
+ // - Traceability (Full mode only, Locked 30): every BR-<slug>-NN id defined in
31
31
  // the Business Rules area is referenced again later (test / acceptance /
32
32
  // analytics). A define-only rule is a WARN (heuristic).
33
33
  // - No whole-section "TBD" / "Not applicable" placeholder bodies (Locked 2);
@@ -43,7 +43,7 @@
43
43
  import { readFileSync } from "node:fs";
44
44
  import { runMain } from "../lib/fatal.mjs";
45
45
 
46
- // A repo-less run (Locked 35) still splits by channel, derived from the evidence:
46
+ // A repo-less run (Locked 34) still splits by channel, derived from the evidence:
47
47
  // "mobile" and "web" are its platform values. "none" is the narrower case where the
48
48
  // evidence describes no interface at all. All three are real values, not missing ones.
49
49
  // "web" is the pre-"web" spelling, still accepted so older documents validate.
@@ -63,7 +63,7 @@ const REQUIRED_SECTIONS = [
63
63
  { key: "references", any: ["References", "Referanslar"] },
64
64
  ];
65
65
 
66
- // Corporate profile backbone (Locked 33). These render even with zero evidence,
66
+ // Corporate profile backbone (Locked 32). These render even with zero evidence,
67
67
  // carrying N/A or EKLENECEK, so their absence is a structural failure rather
68
68
  // than a legitimate omission.
69
69
  const CORPORATE_SECTIONS = [
@@ -290,7 +290,7 @@ function main() {
290
290
  // The development layer is legitimately absent when no repo was selected,
291
291
  // so requiring it there would fail every stack-optional document.
292
292
  .filter((sec) => !(noPlatform && REPO_DEPENDENT_KEYS.has(sec.key)));
293
- const rule = profile === "corporate" ? "Locked 33" : "Locked 2";
293
+ const rule = profile === "corporate" ? "Locked 32" : "Locked 2";
294
294
  for (const sec of requiredSections) {
295
295
  if (!sec.any.some((kw) => headingBlob.includes(kw))) {
296
296
  errors.push(`missing required section (${rule}): ${sec.key}`);
@@ -316,7 +316,7 @@ function main() {
316
316
  //
317
317
  // It ran only in the corporate profile until now, which made the global one the
318
318
  // profile where an inline marker could point at nothing. Two documents already
319
- // told it to: Locked 36 says an unstamped gap fails the dispatch gate with no
319
+ // told it to: Locked 35 says an unstamped gap fails the dispatch gate with no
320
320
  // profile clause, and analysis/render.md routes an external-dependency gap to
321
321
  // `AS-NN` in Section 20 for every run. The check was the only side that
322
322
  // disagreed. Nothing changes for a document that carries no markers: every loop
@@ -341,13 +341,13 @@ function main() {
341
341
  // A gap that names no id cannot be traced to an owner, which is the whole
342
342
  // point of admitting it.
343
343
  if (!/\bAS-\d{2,3}\b/.test(lines[i])) {
344
- errors.push(`line ${i + 1}: EKLENECEK carries no AS-NN reference (Locked 33)`);
344
+ errors.push(`line ${i + 1}: EKLENECEK carries no AS-NN reference (Locked 32)`);
345
345
  }
346
346
  }
347
347
  for (const id of referencedIds) {
348
348
  if (!definedIds.has(id)) {
349
349
  errors.push(
350
- `${id} is referenced in the body but not defined in Risks and Open Questions (Locked 33)`,
350
+ `${id} is referenced in the body but not defined in Risks and Open Questions (Locked 32)`,
351
351
  );
352
352
  }
353
353
  }
@@ -358,7 +358,7 @@ function main() {
358
358
  const row = risksBlock.split("\n").find((l) => l.includes(id)) || "";
359
359
  if (!/(no body reference|govde referansi yok)/i.test(row)) {
360
360
  errors.push(
361
- `${id} is defined in Risks and Open Questions but never referenced in the body (Locked 33)`,
361
+ `${id} is defined in Risks and Open Questions but never referenced in the body (Locked 32)`,
362
362
  );
363
363
  }
364
364
  }
@@ -394,7 +394,7 @@ function main() {
394
394
  const owner = cells[oi] || "";
395
395
  if (!owner || /^(-|\?|TBD|EKLENECEK|N\/A)$/i.test(owner)) {
396
396
  const id = (row.match(/\bAS-\d{2,3}\b/) || ["a row"])[0];
397
- errors.push(`${id} names no owner in Risks and Open Questions (Locked 36)`);
397
+ errors.push(`${id} names no owner in Risks and Open Questions (Locked 35)`);
398
398
  }
399
399
  }
400
400
  } else if (rows.some((l) => /\bAS-\d{2,3}\b/.test(l))) {
@@ -439,7 +439,7 @@ function main() {
439
439
  }
440
440
  }
441
441
  for (const id of [...openMarkers].sort()) {
442
- errors.push(`status: final but ${id} is still open (Locked 36 closure gate)`);
442
+ errors.push(`status: final but ${id} is still open (Locked 35 closure gate)`);
443
443
  }
444
444
  const stillOpen =
445
445
  risksStatusIdx >= 0
@@ -489,7 +489,7 @@ function main() {
489
489
  if (mode === "full") {
490
490
  const matrixStart = lines.findIndex((l) => /^#{2,4}\s/.test(l) && MATRIX_HEADING.test(l));
491
491
  if (matrixStart === -1) {
492
- errors.push("corporate Full mode requires the requirement traceability matrix (Locked 33)");
492
+ errors.push("corporate Full mode requires the requirement traceability matrix (Locked 32)");
493
493
  } else {
494
494
  let matrixBlock = "";
495
495
  for (let i = matrixStart + 1; i < lines.length; i++) {
@@ -577,7 +577,7 @@ function main() {
577
577
  headingBlobLower.includes("ui-test");
578
578
  if (!hasUi) {
579
579
  errors.push(
580
- "front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 31)",
580
+ "front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 30)",
581
581
  );
582
582
  }
583
583
  }
@@ -600,7 +600,7 @@ function main() {
600
600
  // It is Section 15 in the global profile and Section 19 in the corporate one;
601
601
  // the check matches on the title, so only the stack-optional case needs an
602
602
  // exemption - there the whole development layer is legitimately absent
603
- // (Locked 35) and there is no repo for a test to run against yet.
603
+ // (Locked 34) and there is no repo for a test to run against yet.
604
604
  if (mode === "full" && !noPlatform) {
605
605
  const hasTestPlan = ["Test Planı", "Test Plani", "Test Plan"].some((kw) =>
606
606
  headingBlob.includes(kw),
@@ -614,7 +614,7 @@ function main() {
614
614
  warns.push(`${msg} - allowed for backend only when no contract testing is planned`);
615
615
  } else {
616
616
  errors.push(
617
- `missing Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 31)`,
617
+ `missing Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 30)`,
618
618
  );
619
619
  }
620
620
  } else {
@@ -624,7 +624,7 @@ function main() {
624
624
  headingBlobLower.includes("unit test");
625
625
  if (!hasUnit && /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/i.test(text)) {
626
626
  errors.push(
627
- "Section 15 has no unit-test sub-table while business rules are defined (Locked 31)",
627
+ "Section 15 has no unit-test sub-table while business rules are defined (Locked 30)",
628
628
  );
629
629
  }
630
630
  }
@@ -755,7 +755,7 @@ function main() {
755
755
  const tested = new Set((testBody.join("\n").match(idRx) || []).map((x) => x.toUpperCase()));
756
756
  for (const id of defined) {
757
757
  if (!tested.has(id)) {
758
- errors.push(`business rule ${id} has no Section 15 test scenario (Locked 31)`);
758
+ errors.push(`business rule ${id} has no Section 15 test scenario (Locked 30)`);
759
759
  }
760
760
  }
761
761
  }
@@ -763,7 +763,7 @@ function main() {
763
763
 
764
764
  mark("rule-to-test");
765
765
 
766
- // 2h. Redesign mode (Locked 37). Eight checks, live only when the front-matter
766
+ // 2h. Redesign mode (Locked 36). Eight checks, live only when the front-matter
767
767
  // opts in. The contract they enforce is analysis/redesign.md, which loads only
768
768
  // on a redesign run; what is here is the enforcement, not the explanation.
769
769
  const redesign = String(parsed?.fm?.redesign || "false").toLowerCase() === "true";
@@ -787,7 +787,7 @@ function main() {
787
787
  [epBody, "4.6 Endpoint Mapping"],
788
788
  [dlBody, "9.5 Difference List"],
789
789
  ]) {
790
- if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 37)`);
790
+ if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 36)`);
791
791
  }
792
792
 
793
793
  const STATUSES = ["Moved", "Partial", "Missing", "New", "Out of scope"];
@@ -804,7 +804,7 @@ function main() {
804
804
  // the row that was not marked.
805
805
  if (!/:\d+\b/.test(evidence) && !certainty.includes("uncertain")) {
806
806
  errors.push(
807
- `current-behaviour row ${id} cites no file:line and is not marked uncertain (Locked 37)`,
807
+ `current-behaviour row ${id} cites no file:line and is not marked uncertain (Locked 36)`,
808
808
  );
809
809
  }
810
810
  // 3. A rule that lives only in a shared module legitimately reads this way,
@@ -825,14 +825,14 @@ function main() {
825
825
  // 4. A controlled vocabulary decaying into free text.
826
826
  if (!status) {
827
827
  errors.push(
828
- `difference-list row ${id} has status "${statusCell}"; expected one of ${STATUSES.join(" / ")} (Locked 37)`,
828
+ `difference-list row ${id} has status "${statusCell}"; expected one of ${STATUSES.join(" / ")} (Locked 36)`,
829
829
  );
830
830
  continue;
831
831
  }
832
832
  // 5. The reason the mode exists: a behaviour v2 drops with no decision.
833
833
  if ((status === "Missing" || status === "Partial") && !/\bAS-\d{2,3}\b/.test(row)) {
834
834
  errors.push(
835
- `difference-list row ${id} is ${status} with no AS-NN open question (Locked 37)`,
835
+ `difference-list row ${id} is ${status} with no AS-NN open question (Locked 36)`,
836
836
  );
837
837
  }
838
838
  }
@@ -866,7 +866,7 @@ function main() {
866
866
  const cellCount = (line) =>
867
867
  line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
868
868
  if (!header) {
869
- errors.push("Section 4.6 has no v1/v2 header row (Locked 37)");
869
+ errors.push("Section 4.6 has no v1/v2 header row (Locked 36)");
870
870
  } else {
871
871
  const cols = cellCount(header);
872
872
  for (const row of tableDataRows(epBody)) {
@@ -925,7 +925,7 @@ function main() {
925
925
 
926
926
  // 4. Placeholder whole-section bodies (Locked 2).
927
927
  // Not a defect in the corporate profile: there N/A and EKLENECEK are how a
928
- // backbone section reports "considered, nothing to state" (Locked 33), and the
928
+ // backbone section reports "considered, nothing to state" (Locked 32), and the
929
929
  // EKLENECEK-to-open-question pairing above is what keeps them honest.
930
930
  if (profile === "global" && /^\s*(TBD|Not applicable|N\/A)\s*$/im.test(text)) {
931
931
  warns.push(
@@ -992,7 +992,7 @@ function main() {
992
992
  for (const [id, n] of counts) {
993
993
  if (n < 2) {
994
994
  warns.push(
995
- `business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 31 traceability`,
995
+ `business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 30 traceability`,
996
996
  );
997
997
  }
998
998
  }
@@ -1007,7 +1007,7 @@ function main() {
1007
1007
  if (!/^\|\s*MT-[a-z0-9-]+/i.test(line)) continue;
1008
1008
  if (!/BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/i.test(line)) {
1009
1009
  const id = (line.match(/MT-[a-z0-9-]+/i) || ["MT row"])[0];
1010
- errors.push(`manual scenario ${id} has no BR- id (Section 15.7 / Locked 31)`);
1010
+ errors.push(`manual scenario ${id} has no BR- id (Section 15.7 / Locked 30)`);
1011
1011
  }
1012
1012
  }
1013
1013
 
@@ -1034,13 +1034,188 @@ function main() {
1034
1034
  if (/^-+$/.test(cells[1]) || /Bile\u015fen|Component/i.test(cells[1])) continue;
1035
1035
  if (!cells[3] || !cells[4]) {
1036
1036
  errors.push(
1037
- `variant row "${cells[1]}" is missing the all-values or used-here column (Section 6.X / Locked 29)`,
1037
+ `variant row "${cells[1]}" is missing the all-values or used-here column (Section 6.X / Locked 28)`,
1038
1038
  );
1039
1039
  }
1040
1040
  }
1041
1041
 
1042
1042
  mark("variant-coverage");
1043
1043
 
1044
+ // 2h. Locked 13, 14, 15, 17, 18 and 21.
1045
+ //
1046
+ // Each checks a SHAPE the template prescribes. None judges content: a gate
1047
+ // that guesses at meaning fails honest documents, and a gate people learn to
1048
+ // ignore is worse than the prose it replaced.
1049
+ //
1050
+ // Each is keyed off a structure only a real document carries, so a minimal
1051
+ // document is skipped rather than failed. "Section 4 exists" is not evidence
1052
+ // that anybody wrote scenarios, and "Section 4 has a 4.1 sub-heading" is.
1053
+
1054
+ // Locked 13: Section 4 scenarios are Gherkin, not prose.
1055
+ const storiesBody = sectionBody(allLines, [
1056
+ "Kullanıcı Hikayeleri",
1057
+ "Kullanici Hikayeleri",
1058
+ "User Stories",
1059
+ ]);
1060
+ if (storiesBody) {
1061
+ const blocks = [];
1062
+ let inFence = false;
1063
+ let isGherkin = false;
1064
+ let buf = [];
1065
+ for (const line of storiesBody) {
1066
+ if (line.trim().startsWith("```")) {
1067
+ if (!inFence) {
1068
+ inFence = true;
1069
+ isGherkin = /^```\s*gherkin/i.test(line.trim());
1070
+ buf = [];
1071
+ } else {
1072
+ if (isGherkin) blocks.push(buf.join("\n"));
1073
+ inFence = false;
1074
+ }
1075
+ continue;
1076
+ }
1077
+ if (inFence && isGherkin) buf.push(line);
1078
+ }
1079
+ // Scenarios are 4.1 to 4.3. 4.4 is the business-rule table, and a redesign
1080
+ // run adds 4.5 and up (Locked 36: current behaviour, endpoint mapping,
1081
+ // difference list) - none of which is a scenario, so "everything except
1082
+ // 4.4" would demand Gherkin of three tables.
1083
+ const scenarioHeads = storiesBody.filter((l) => /^#{3}\s+4\.[1-3]\b/.test(l));
1084
+ if (scenarioHeads.length && !blocks.length) {
1085
+ errors.push(
1086
+ `Section 4 has ${scenarioHeads.length} scenario sub-section(s) and no \`\`\`gherkin block; plain-prose user stories are rejected (Locked 13)`,
1087
+ );
1088
+ }
1089
+ blocks.forEach((b, i) => {
1090
+ const missing = ["Given", "When", "Then"].filter(
1091
+ (k) => !new RegExp(`(^|\\n)\\s*${k}\\b`).test(b),
1092
+ );
1093
+ if (missing.length) {
1094
+ errors.push(`Section 4 gherkin block ${i + 1} states no ${missing.join(", ")} (Locked 13)`);
1095
+ }
1096
+ });
1097
+ }
1098
+
1099
+ mark("gherkin-scenarios", storiesBody ? undefined : "no User Stories section");
1100
+
1101
+ // Locked 14: Goals and Non-Goals are paired. A goal with an empty counterpart
1102
+ // cell is the failure the decision names - omission cannot be read as
1103
+ // exclusion, so the column has to say what is NOT covered.
1104
+ const goalsBody = sectionBody(allLines, [
1105
+ "Hedefler ve Karşı Hedefler",
1106
+ "Hedefler ve Karsi Hedefler",
1107
+ "Goals and Non-Goals",
1108
+ "Goals",
1109
+ ]);
1110
+ let goalRows = 0;
1111
+ if (goalsBody) {
1112
+ for (const row of tableDataRows(goalsBody)) {
1113
+ const cells = row
1114
+ .split("|")
1115
+ .map((c) => c.trim())
1116
+ .filter((c, i, a) => i !== 0 && i !== a.length - 1);
1117
+ if (cells.length < 2) continue;
1118
+ goalRows++;
1119
+ if (!cells[1] || cells[1] === "-") {
1120
+ errors.push(`goal "${cells[0].slice(0, 40)}" has no paired non-goal (Locked 14)`);
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ mark("goals-paired", goalRows ? undefined : "no Goals/Non-Goals table");
1126
+
1127
+ // Locked 15: a new asset that is not SVG owes a rationale. The decision
1128
+ // allows Lottie and optimized PNG - it does not allow them silently.
1129
+ const assetsBody = sectionBody(allLines, ["Asset Envanteri", "Asset Inventory"]);
1130
+ let assetRows = 0;
1131
+ if (assetsBody) {
1132
+ for (const row of tableDataRows(assetsBody)) {
1133
+ const cells = row
1134
+ .split("|")
1135
+ .map((c) => c.trim())
1136
+ .filter((c, i, a) => i !== 0 && i !== a.length - 1);
1137
+ if (cells.length < 5) continue;
1138
+ assetRows++;
1139
+ const [name, format, existing, , notes] = cells;
1140
+ const isNew = /^(no|hayır|hayir)\b/i.test(existing);
1141
+ if (!isNew) continue;
1142
+ if (/\bsvg\b/i.test(format)) continue;
1143
+ if (!notes || !/\S/.test(notes.replace(/^new,?\s*/i, ""))) {
1144
+ errors.push(
1145
+ `new asset "${name.slice(0, 40)}" is ${format} rather than SVG and states no exception rationale (Locked 15)`,
1146
+ );
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ mark("asset-format", assetRows ? undefined : "no Asset Inventory table");
1152
+
1153
+ // Locked 17: "other errors" is the phrase the decision exists to reject. An
1154
+ // exhaustive list cannot be checked for exhaustiveness from inside the
1155
+ // document, but the admission that it is not exhaustive can.
1156
+ const apiBody = sectionBody(allLines, ["API Kontratları", "API Kontratlari", "API Contracts"]);
1157
+ if (apiBody) {
1158
+ const VAGUE = [
1159
+ /\bother errors?\b/i,
1160
+ /\bvarious \dxx\b/i,
1161
+ /\bdiğer hatalar\b/i,
1162
+ /\bdiger hatalar\b/i,
1163
+ /\betc\.?\s*\|/i,
1164
+ ];
1165
+ for (const line of apiBody) {
1166
+ const hit = VAGUE.find((rx) => rx.test(line));
1167
+ if (hit) {
1168
+ errors.push(
1169
+ `Section 9 stands in for a status code instead of listing it: "${line.trim().slice(0, 60)}" (Locked 17)`,
1170
+ );
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ mark("api-variants-exhaustive", apiBody ? undefined : "no API Contracts section");
1176
+
1177
+ // Locked 18: screenshots are embedded, not linked. A remote image survives
1178
+ // exactly as long as whatever serves it, and the document outlives that.
1179
+ const designBody = sectionBody(allLines, [
1180
+ "Tasarım Referansı",
1181
+ "Tasarim Referansi",
1182
+ "Design Reference",
1183
+ ]);
1184
+ if (designBody) {
1185
+ for (const line of designBody) {
1186
+ const m = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/.exec(line);
1187
+ if (m) {
1188
+ errors.push(
1189
+ `Section 5 links a remote screenshot instead of embedding a local file: ${m[1].slice(0, 60)} (Locked 18)`,
1190
+ );
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ mark("screenshots-embedded", designBody ? undefined : "no Design Reference section");
1196
+
1197
+ // Locked 21: References sit at the bottom. The reader sees goals, flow and
1198
+ // design before sources, which is only true if no numbered section follows.
1199
+ const topNums = [];
1200
+ let refNum = null;
1201
+ for (const line of allLines) {
1202
+ const m = /^##\s+(\d+)\.\s+(.*)$/.exec(line);
1203
+ if (!m) continue;
1204
+ const n = Number(m[1]);
1205
+ topNums.push(n);
1206
+ if (/Referanslar|References/i.test(m[2])) refNum = n;
1207
+ }
1208
+ if (refNum !== null && topNums.length) {
1209
+ const highest = Math.max(...topNums);
1210
+ if (refNum !== highest) {
1211
+ errors.push(
1212
+ `References is Section ${refNum} but Section ${highest} follows it, and sources go last (Locked 21)`,
1213
+ );
1214
+ }
1215
+ }
1216
+
1217
+ mark("references-last", refNum === null ? "no References section" : undefined);
1218
+
1044
1219
  // Report
1045
1220
  if (report) {
1046
1221
  const width = Math.max(...checkRows.map((r) => r.id.length));
@@ -6,7 +6,7 @@
6
6
  * Two surfaces in this pipeline require a citation and neither has ever checked
7
7
  * one. `reviewer-output.schema.json` makes `file` and `line` required on every
8
8
  * finding; `analysis-spec.schema.json` carries `repoEvidence` entries of
9
- * `{name, file, line, tag}` and `analysis/locked.md` (Locked 37) asks for
9
+ * `{name, file, line, tag}` and `analysis/locked.md` (Locked 36) asks for
10
10
  * `repo/file:line` on every current-behaviour row. The only check that existed
11
11
  * was `validate-analysis-doc.mjs`'s `/:\d+\b/` - a test that the row LOOKS like
12
12
  * a citation, satisfied by `Foo.swift:9999` in a repo with no Foo.swift.
@@ -441,6 +441,38 @@ async function main() {
441
441
  const tmp = `${dirname(path)}/.${basename(path)}.${process.pid}.tmp`;
442
442
  writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n");
443
443
  renameSync(tmp, path); // atomic on POSIX
444
+
445
+ // The check above is a time-of-check, this is the time-of-use. No POSIX
446
+ // call renames a file conditionally on still holding a lock, so between
447
+ // stillOurs() and renameSync() there is a window - narrow, but real, and it
448
+ // widens exactly when the machine is loaded enough for a holder to be
449
+ // descheduled past the reclaim ceiling.
450
+ //
451
+ // Reading the file back closes it after the fact rather than before: our
452
+ // rev is the one we just wrote, so a different rev on disk means another
453
+ // writer's rename landed on top of ours. That is the difference between
454
+ // reporting success while losing an update - the one outcome this script
455
+ // exists to prevent - and refusing loudly enough for the caller to retry.
456
+ try {
457
+ const landed = JSON.parse(readFileSync(path, "utf-8"));
458
+ if (landed?.rev !== next.rev) {
459
+ console.error(
460
+ `write-state: our update was overwritten between the write and this check ` +
461
+ `(wrote rev ${next.rev}, found rev ${landed?.rev}). Re-read the state and retry.`,
462
+ );
463
+ releaseLock(path);
464
+ process.exit(4);
465
+ }
466
+ } catch (err) {
467
+ // An unreadable file here is not our write failing - it is the state
468
+ // being clobbered by something that is not this writer. Saying so beats
469
+ // exiting 0 over a file we can no longer parse.
470
+ console.error(
471
+ `write-state: the state was unreadable immediately after our write - ${err.message}`,
472
+ );
473
+ releaseLock(path);
474
+ process.exit(4);
475
+ }
444
476
  } catch (err) {
445
477
  console.error(`write-state: ${err.message}`);
446
478
  releaseLock(path);
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-18T07:17:50Z",
3
+ "generatedAt": "2026-09-20T20:24:07Z",
4
4
  "skillCount": 216,
5
5
  "entries": [
6
6
  {
7
7
  "path": "shared/core/apple-archive-compliance/SKILL.md",
8
- "sha256": "e5c38c240a674903acf3f32e5b492a4a6c5dc438a323bb3ca9adc361b41a6884"
8
+ "sha256": "6675a53441f2f3bedc82284998f402bd51fa251c7491a595e12c6c4016830337"
9
9
  },
10
10
  {
11
11
  "path": "shared/core/google-play-compliance/SKILL.md",
12
- "sha256": "49412b53af57ba31fe7d19983144043a2333249d075ff90a9fde9a9d3482ce83"
12
+ "sha256": "2229d7b732b0523142b9c467049931b1d100343c1e318428f49e3fea73fc39ec"
13
13
  },
14
14
  {
15
15
  "path": "shared/core/multi-agent-analysis-jira/SKILL.md",
@@ -253,7 +253,7 @@
253
253
  },
254
254
  {
255
255
  "path": "shared/core/multi-agent/SKILL.md",
256
- "sha256": "849a2c7db9fb779d9919c7ed6cf3e0c3c6fd105ff961c45af31055ad21fd181c"
256
+ "sha256": "9ab7af2ffb01f8eb0fae624ae40ea9ff2f6d69ad61544bd266b1b97f60eb4a0f"
257
257
  },
258
258
  {
259
259
  "path": "shared/external/accessibility-compliance-accessibility-audit/SKILL.md",
@@ -669,7 +669,7 @@
669
669
  },
670
670
  {
671
671
  "path": "shared/external/signal-community/SKILL.md",
672
- "sha256": "98a5883811a58a352f229efaa2ff785bcb7f8174988a5d6cc8d0d57cabc569cf"
672
+ "sha256": "d5989a85c39856e451ce5e5f9dd5a250be02067c56e47b8667a4f2d78014a89c"
673
673
  },
674
674
  {
675
675
  "path": "shared/external/skill-creator/SKILL.md",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: apple-archive-compliance
3
3
  language: en
4
- description: "Apple App Store Review compliance - wraps the multi-agent-toolkit-mcp `ios_app_store_audit` tool (18-rule deep scan) with ITMS error code mapping + EN/TR humanizer templates. Consumed by: /multi-agent:test 'store-ready' (primary dispatch), Phase 4 Security Auditor (catalog cross-ref on Info.plist / PrivacyInfo / entitlements diffs), /multi-agent:review (PR citation), /multi-agent:channels (PR body auto-augmentation). Wiring enforced by smoke-compliance-skills.sh. Use when an .xcarchive needs an App Store review check before submission, or a rejection has to be diagnosed."
4
+ description: "Apple App Store Review compliance - wraps the multi-agent-toolkit-mcp `ios_app_store_audit` tool (18-rule deep scan) with ITMS error code mapping + EN/TR humanizer templates. Consumed by: /multi-agent:test 'store-ready' (primary dispatch), Phase 3 Security Auditor (catalog cross-ref on Info.plist / PrivacyInfo / entitlements diffs), /multi-agent:review (PR citation), /multi-agent:channels (PR body auto-augmentation). Wiring enforced by smoke-compliance-skills.sh. Use when an .xcarchive needs an App Store review check before submission, or a rejection has to be diagnosed."
5
5
  user-invocable: true
6
6
  argument-hint: '[archive-path] [--threshold=error|warning|info] [--format=markdown|json] [--lang=en|tr] - no-arg: picks most recent .xcarchive from ~/Library/Developer/Xcode/Archives/'
7
7
  standards-registry: references/rules.yml
@@ -18,7 +18,7 @@ Pre-submission compliance check for iOS `.xcarchive` bundles. Wraps the [`ios_ap
18
18
  | Consumer | How it uses this skill |
19
19
  |---|---|
20
20
  | `/multi-agent:test "store-ready"` (iOS) | Main entry point - runs the binary, renders the full report |
21
- | Phase 4 Security Auditor (iOS review) | Reads the rule catalog for cross-reference when diff touches Info.plist, PrivacyInfo.xcprivacy, or entitlements |
21
+ | Phase 3 Security Auditor (iOS review) | Reads the rule catalog for cross-reference when diff touches Info.plist, PrivacyInfo.xcprivacy, or entitlements |
22
22
  | `/multi-agent:review` | On iOS diffs, cites guideline refs next to flagged symbols |
23
23
  | `/multi-agent:channels` | When PR touches release-relevant files, appends compliance warnings to PR body |
24
24
 
@@ -208,7 +208,7 @@ sim-test.md's "store-ready" scenario on iOS:
208
208
  4. Render humanizer template per `--lang` flag (default `en`; `promptLanguage` is locked to `"en"`)
209
209
  5. Exit with `1` if any blockers, `0` otherwise (CI-friendly)
210
210
 
211
- ### Phase 4 Security Auditor (optional, on-demand)
211
+ ### Phase 3 Security Auditor (optional, on-demand)
212
212
 
213
213
  When reviewing an iOS PR, if diff touches ANY of:
214
214
  - `*/Info.plist`
@@ -261,12 +261,12 @@ multi-agent-test "store-ready" --format=json --auto-pick
261
261
  mcp__multi-agent-toolkit__ios_app_store_audit({ archive_path: "~/Library/Developer/Xcode/Archives/2026-04-21/MyApp.xcarchive", rules: "all" })
262
262
  ```
263
263
 
264
- ### Phase 4 Security Auditor (catalog cross-ref, no binary invocation)
264
+ ### Phase 3 Security Auditor (catalog cross-ref, no binary invocation)
265
265
 
266
266
  When a Phase 3 review diff touches iOS release-relevant paths (`Info.plist`, `PrivacyInfo.xcprivacy`, `*.entitlements`, `AppDelegate.swift`, `project.pbxproj`), the security-auditor persona loads this skill's 18-rule catalog and cites matching ruleIDs next to its findings. No binary invocation at review time - catalog reads are free.
267
267
 
268
268
  ```
269
- # Example finding from Phase 4 Security Auditor (wired in v5.8.1):
269
+ # Example finding from Phase 3 Security Auditor (wired in v5.8.1):
270
270
  [BLOCKING] Info.plist: new NSCameraUsageDescription without justification
271
271
  (apple-archive-compliance / info-plist - Guideline 5.1.1)
272
272
  File: MyApp/Info.plist:42
@@ -312,7 +312,7 @@ The appended section is structured:
312
312
  ## Version history
313
313
 
314
314
  - **v5.8.0** - Skill introduced. Wraps the (now-retired) standalone ArchiveGuard binary, 17-rule catalog, EN+TR humanizer. Primary consumer only (`/multi-agent:test "store-ready"`).
315
- - **v5.8.1** - 3 additional consumer surfaces wired (Phase 4 Security Auditor cross-ref, `/multi-agent:review` citation, `/multi-agent:channels` PR-body auto-augmentation). Regression-locked by `smoke-compliance-skills.sh` (45 assertions).
315
+ - **v5.8.1** - 3 additional consumer surfaces wired (Phase 3 Security Auditor cross-ref, `/multi-agent:review` citation, `/multi-agent:channels` PR-body auto-augmentation). Regression-locked by `smoke-compliance-skills.sh` (45 assertions).
316
316
  - **v6.0.0** - `language: en` frontmatter tag; 4-consumer Examples section expanded.
317
317
  - **v8.4.0** - Migrated off the standalone ArchiveGuard Swift binary. Backend is now the `ios_app_store_audit` tool inside `@mmerterden/multi-agent-toolkit-mcp` ≥ v2.4 (full Node port of the 17 rules). All four consumer surfaces unchanged; rule IDs and JSON output shape preserved for backward compatibility. Rule catalog now points to `multi-agent-toolkit-mcp/tools/ios-app-store-audit/rules/`. ArchiveGuard repo deprecated.
318
318
  - **Unreleased** - Catalog grows to 18 rules: adds `sdk-floor` (ITMS-90725, iOS 26 / Xcode 26 build floor in force since 2026-04-28), which nothing previously checked. `embedded-sdk`'s missing-framework-privacy-manifest finding moves from a `5.1.1` WARNING to `ITMS-91061` ERROR, an enforced rejection since 2025-02-12 - archives that used to pass with a warning now fail. Requires `@mmerterden/multi-agent-toolkit-mcp` ≥ v3.0.0.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: google-play-compliance
3
3
  language: en
4
- description: "Google Play Store publication compliance - bundletool + aapt2 + apksigner orchestration + 21-rule policy catalog with Play Console error codes + humanizer templates. Consumed by: /multi-agent:test 'store-ready' (primary dispatch), Phase 4 Security Auditor (catalog cross-ref on AndroidManifest / build.gradle / proguard / signing diffs), /multi-agent:review (PR citation), /multi-agent:channels (PR body auto-augmentation). Wiring enforced by smoke-compliance-skills.sh. Use when an .aab or .apk needs a Play policy check before upload, or a Play rejection has to be diagnosed."
4
+ description: "Google Play Store publication compliance - bundletool + aapt2 + apksigner orchestration + 21-rule policy catalog with Play Console error codes + humanizer templates. Consumed by: /multi-agent:test 'store-ready' (primary dispatch), Phase 3 Security Auditor (catalog cross-ref on AndroidManifest / build.gradle / proguard / signing diffs), /multi-agent:review (PR citation), /multi-agent:channels (PR body auto-augmentation). Wiring enforced by smoke-compliance-skills.sh. Use when an .aab or .apk needs a Play policy check before upload, or a Play rejection has to be diagnosed."
5
5
  user-invocable: true
6
6
  argument-hint: '[bundle-path] [--threshold=error|warning|info] [--format=markdown|json] [--lang=en|tr] - no-arg: picks most recent .aab from app/build/outputs/bundle/release/'
7
7
  standards-registry: references/rules.yml
@@ -18,7 +18,7 @@ Pre-submission compliance check for Android App Bundle (`.aab`) and APK artifact
18
18
  | Consumer | How it uses this skill |
19
19
  |---|---|
20
20
  | `/multi-agent:test "store-ready"` (Android) | Main entry point - runs the tools, renders the full report |
21
- | Phase 4 Security Auditor (Android review) | Reads the rule catalog for cross-reference when diff touches AndroidManifest.xml, gradle config, or signing |
21
+ | Phase 3 Security Auditor (Android review) | Reads the rule catalog for cross-reference when diff touches AndroidManifest.xml, gradle config, or signing |
22
22
  | `/multi-agent:review` | On Android diffs, cites Play policy refs next to flagged changes (e.g. new `MANAGE_EXTERNAL_STORAGE` without justification) |
23
23
  | `/multi-agent:channels` | When PR is release-relevant, appends compliance warnings to PR body |
24
24
 
@@ -232,7 +232,7 @@ sim-test.md's "store-ready" scenario on Android:
232
232
  5. Render humanizer template per `--lang` flag (default `en`; `promptLanguage` is locked to `"en"`)
233
233
  6. Exit with `1` if any blockers, `0` otherwise (CI-friendly)
234
234
 
235
- ### Phase 4 Security Auditor (optional, on-demand)
235
+ ### Phase 3 Security Auditor (optional, on-demand)
236
236
 
237
237
  When reviewing an Android PR, if diff touches ANY of:
238
238
  - `**/AndroidManifest.xml` (permissions / exported / targetSdk)
@@ -286,12 +286,12 @@ multi-agent-test "store-ready" --last-version-code=142 --auto-pick
286
286
  /multi-agent:test "store-ready" --format=json --auto-pick
287
287
  ```
288
288
 
289
- ### Phase 4 Security Auditor (catalog cross-ref, no tool invocation)
289
+ ### Phase 3 Security Auditor (catalog cross-ref, no tool invocation)
290
290
 
291
291
  When a Phase 3 review diff touches Android release-relevant paths (`AndroidManifest.xml`, `build.gradle(.kts)`, `proguard-rules.pro`, `network_security_config.xml`), the security-auditor persona loads this skill's 21-rule policy catalog and cites matching ruleIDs next to its findings. No bundletool / aapt2 / apksigner invocation at review time - catalog reads are free.
292
292
 
293
293
  ```
294
- # Example finding from Phase 4 Security Auditor (wired in v5.8.1):
294
+ # Example finding from Phase 3 Security Auditor (wired in v5.8.1):
295
295
  [BLOCKING] AndroidManifest.xml: new MANAGE_EXTERNAL_STORAGE permission added
296
296
  (google-play-compliance / dangerous-permissions - Policy - Permissions)
297
297
  File: app/src/main/AndroidManifest.xml:12
@@ -345,5 +345,5 @@ The appended section is structured:
345
345
  ## Version history
346
346
 
347
347
  - **v5.8.0** - Skill introduced. Orchestrates bundletool + aapt2 + apksigner, 21-rule catalog, EN+TR humanizer. Primary consumer only (`/multi-agent:test "store-ready"`).
348
- - **v5.8.1** - 3 additional consumer surfaces wired (Phase 4 Security Auditor cross-ref, `/multi-agent:review` citation, `/multi-agent:channels` PR-body auto-augmentation). Regression-locked by `smoke-compliance-skills.sh` (45 assertions).
348
+ - **v5.8.1** - 3 additional consumer surfaces wired (Phase 3 Security Auditor cross-ref, `/multi-agent:review` citation, `/multi-agent:channels` PR-body auto-augmentation). Regression-locked by `smoke-compliance-skills.sh` (45 assertions).
349
349
  - **v6.0.0** - `language: en` frontmatter tag; 4-consumer Examples section expanded.
@@ -204,28 +204,29 @@ This works across sessions because:
204
204
 
205
205
  Every agent persona under `$HOME/.claude/agents/*.md` declares a `preferredModel` frontmatter field. The orchestrator MUST honor it on every dispatch, on both Claude Code and Copilot CLI - cross-CLI parity is enforced by `smoke-cross-cli-behavior.sh`.
206
206
 
207
- | Persona | `preferredModel` | Rationale (condensed) |
208
- |---|---|---|
209
- | `explorer` | `sonnet` | Scan/pattern work, cost-efficient without quality loss |
210
- | `code-reviewer` | `fable` | Reviewer 1; Phase 3 sets Reviewer 2 `opus`, Reviewer 3 `sonnet` |
211
- | `security-auditor` | `opus` | False-negative cost is high; opus reduces miss rate |
212
- | `ios-architect` | `fable` | Deep module/protocol/migration reasoning |
213
- | `android-architect` | `fable` | Gradle modules, Compose stability, Hilt graphs |
214
- | `backend-architect` | `fable` | API + migration + scaling reasoning |
207
+ Each persona file carries its own `modelRationale`; this is the rung only.
215
208
 
216
- **Dispatch contract (pseudo-code):**
209
+ | Persona | `preferredModel` |
210
+ |---|---|
211
+ | `explorer` | `sonnet` |
212
+ | `code-reviewer` | `fable` (Reviewer 1; Phase 3 sets Reviewer 2 `opus`, Reviewer 3 `sonnet`) |
213
+ | `security-auditor` | `opus` |
214
+ | `ios-architect` / `android-architect` / `backend-architect` | `fable` |
215
+
216
+ **Dispatch contract:**
217
217
 
218
218
  ```bash
219
219
  persona_file="$HOME/.claude/agents/${persona}.md"
220
220
  pref_model="$(awk '/^preferredModel:/ {print $2; exit}' "$persona_file")"
221
- override="${PHASE_MODEL_OVERRIDE:-}" # set by orchestrator per-dispatch (e.g. Phase 3 Reviewer 3 → sonnet)
222
- resolved_model="${override:-${pref_model:-opus}}"
221
+ override="${PHASE_MODEL_OVERRIDE:-}" # per-dispatch, e.g. Phase 3 Reviewer 3 → sonnet
222
+ routed="${override:-$("$HOME/.claude/lib/model-dispatch.sh" subagent --persona "$persona" --phase "$currentPhase" --task-kind "$taskType" --default "${pref_model:-opus}" 2>/dev/null)}"
223
+ resolved_model="${routed:-${pref_model:-opus}}"
223
224
 
224
225
  export CLAUDE_CODE_SUBAGENT_MODEL="$resolved_model"
225
- # Claude Code honors the env var; Copilot CLI reads the same env + passes as --model flag.
226
+ # Claude Code honors the env var; Copilot CLI also passes it as --model.
226
227
  ```
227
228
 
228
- **Precedence:** per-dispatch `PHASE_MODEL_OVERRIDE` > persona `preferredModel` > global default (`opus`).
229
+ **Precedence:** per-dispatch `PHASE_MODEL_OVERRIDE` > `prefs.global.modelRouting` rule > persona `preferredModel` > global default (`opus`). Routing here is Anthropic-only: features/model-fallback.md "Routing at dispatch".
229
230
 
230
231
  **Observability:** emit `phase.agent.dispatch` OTel span with `{persona, resolved_model, override_source}` attributes when `MULTI_AGENT_OTEL_SPANS=1` (see `phase-tracker.sh`).
231
232
 
@@ -38,4 +38,4 @@ alarmkit, app-clips, app-intents, app-store-optimization, app-store-review, appl
38
38
 
39
39
  avkit, core-data, cryptokit, ios-simulator, pdfkit, swift-api-design-guidelines, swift-architecture, swift-formatstyle, swift-security, swiftlint
40
40
 
41
- Note: `swift-security` is a superset of the older standalone `ios-security` skill; `ios-security` is kept because Phase 4 review references it. Candidate for retirement in a later pass.
41
+ Note: `swift-security` is a superset of the older standalone `ios-security` skill; `ios-security` is kept because Phase 3 review references it. Candidate for retirement in a later pass.