@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.
- package/CHANGELOG.md +121 -0
- package/README.md +3 -3
- package/docs/ecosystem.md +12 -4
- package/docs/facts.json +20 -4
- package/docs/features.md +1 -1
- package/docs/recovery-guide.md +8 -8
- package/manifest.json +64 -62
- package/package.json +1 -1
- package/pipeline/agents/dev-critic.md +4 -4
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +6 -6
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +1 -1
- package/pipeline/lib/model-dispatch.sh +140 -0
- package/pipeline/lib/outbound-gate.mjs +14 -0
- package/pipeline/multi-agent-refs/_dev-context.md +5 -5
- package/pipeline/multi-agent-refs/analysis/evidence.md +2 -2
- package/pipeline/multi-agent-refs/analysis/intake.md +6 -6
- package/pipeline/multi-agent-refs/analysis/locked.md +27 -0
- package/pipeline/multi-agent-refs/analysis/redesign.md +1 -1
- package/pipeline/multi-agent-refs/analysis/render.md +9 -9
- package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
- package/pipeline/multi-agent-refs/analysis/review.md +2 -2
- package/pipeline/multi-agent-refs/analysis/synthesis.md +2 -2
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +9 -9
- package/pipeline/multi-agent-refs/analysis-template.md +19 -19
- package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
- package/pipeline/multi-agent-refs/conventions-defaults.md +2 -2
- package/pipeline/multi-agent-refs/features/analysis-jira.md +1 -1
- package/pipeline/multi-agent-refs/features/doctor.md +1 -1
- package/pipeline/multi-agent-refs/features/model-fallback.md +36 -0
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
- package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +2 -2
- package/pipeline/preferences-template.json +5 -0
- package/pipeline/rules/figma-pipeline.md +8 -8
- package/pipeline/schemas/analysis-output.schema.json +1 -1
- package/pipeline/schemas/analysis-spec.schema.json +2 -2
- package/pipeline/schemas/figma-project-config.schema.json +1 -1
- package/pipeline/schemas/prefs.schema.json +2 -2
- package/pipeline/schemas/secret-patterns.json +124 -0
- package/pipeline/scripts/build-references.mjs +2 -2
- package/pipeline/scripts/bulk-read.sh +10 -1
- package/pipeline/scripts/cost-table.json +8 -1
- package/pipeline/scripts/doctor.mjs +1 -1
- package/pipeline/scripts/gen-facts.mjs +112 -7
- package/pipeline/scripts/phase-tracker.sh +5 -5
- package/pipeline/scripts/pre-commit-check.sh +30 -1
- package/pipeline/scripts/scan-skills.sh +26 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +201 -26
- package/pipeline/scripts/verify-citations.mjs +1 -1
- package/pipeline/scripts/write-state.mjs +32 -0
- package/pipeline/skills/.skill-manifest.json +5 -5
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +6 -6
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +14 -13
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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": "
|
|
8
|
+
"sha256": "6675a53441f2f3bedc82284998f402bd51fa251c7491a595e12c6c4016830337"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "shared/core/google-play-compliance/SKILL.md",
|
|
12
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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
|
+
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:-}" #
|
|
222
|
-
|
|
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
|
|
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
|
|
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.
|