@mmerterden/multi-agent-pipeline 14.2.2 → 15.1.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.
- package/CHANGELOG.md +186 -6
- package/README.md +19 -12
- package/README.tr.md +19 -12
- package/SECURITY.md +43 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +13 -13
- package/docs/ecosystem.md +31 -31
- package/docs/features.md +5 -5
- package/index.js +6 -1
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +109 -3
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +54 -113
- package/install/_plugin-skills.mjs +36 -36
- package/install/claude.mjs +251 -61
- package/install/codex.mjs +28 -6
- package/install/copilot.mjs +69 -9
- package/install/index.mjs +9 -3
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +2 -3
- package/pipeline/commands/multi-agent/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +62 -40
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +4 -4
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +316 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/operations.md +7 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +3 -2
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +18 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +296 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/README.md +4 -3
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +4 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +28 -60
- package/pipeline/scripts/check-derived-drift.mjs +55 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +12 -2
- package/pipeline/scripts/migrate-prefs.mjs +33 -21
- package/pipeline/scripts/phase-tracker.sh +32 -5
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/run-aggregator.mjs +7 -2
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/uninstall.mjs +160 -11
- package/pipeline/scripts/usage-report.mjs +426 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +250 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +449 -12
- package/pipeline/skills/shared/README.md +14 -10
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +89 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
- package/pipeline/skills/skills-index.md +7 -4
|
@@ -115,6 +115,23 @@ need_jq() {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
# Live usage ping (best-effort). Emits one per-phase update to the private
|
|
119
|
+
# dashboard via usage-report.mjs, always status=running so per-phase updates
|
|
120
|
+
# never fold the run into rollup counters - the terminal fold comes only from
|
|
121
|
+
# the Phase 7 / halt emit that reads the real run status. The emitter no-ops
|
|
122
|
+
# unless prefs.global.usageLog.enabled; detached so it never blocks a boundary.
|
|
123
|
+
usage_live_ping() {
|
|
124
|
+
local task="$1" phase="$2"
|
|
125
|
+
local script="$HOME/.claude/scripts/usage-report.mjs"
|
|
126
|
+
local prefs="$HOME/.claude/multi-agent-preferences.json"
|
|
127
|
+
[ -n "$task" ] && [ -f "$script" ] || return 0
|
|
128
|
+
# Cheap gate: only spawn the emitter when usage logging is actually on, so a
|
|
129
|
+
# user who never enabled it pays nothing per phase boundary. jq is already a
|
|
130
|
+
# hard dependency of this script.
|
|
131
|
+
jq -e '.global.usageLog.enabled == true' "$prefs" >/dev/null 2>&1 || return 0
|
|
132
|
+
( node "$script" --task-id "$task" --phase "$phase" --status running >/dev/null 2>&1 & ) 2>/dev/null || true
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
# Decode HTML entities in a tile title.
|
|
119
136
|
#
|
|
120
137
|
# `rules.md` forbids entities in "titles, commit messages, task subjects, or body
|
|
@@ -382,6 +399,9 @@ load_state() {
|
|
|
382
399
|
fi
|
|
383
400
|
}
|
|
384
401
|
save_state() {
|
|
402
|
+
# Refuse to persist an empty document: every caller pipes jq output in, and
|
|
403
|
+
# a failed jq yields "" - writing that would destroy the whole tracker state.
|
|
404
|
+
[ -n "$1" ] || { echo "save_state: refusing to write empty state" >&2; return 65; }
|
|
385
405
|
mkdir -p "$TRACKER_DIR" 2>/dev/null
|
|
386
406
|
printf '%s\n' "$1" > "${TRACKER_FILE}.tmp"
|
|
387
407
|
mv "${TRACKER_FILE}.tmp" "$TRACKER_FILE"
|
|
@@ -678,6 +698,7 @@ case "$ACTION" in
|
|
|
678
698
|
# subsequent tracker call (the pointer flips on the most recent init).
|
|
679
699
|
echo "$TRACKER_FILE" > "$HOME/.claude/logs/multi-agent/.tracker-current"
|
|
680
700
|
render
|
|
701
|
+
usage_live_ping "$TASK_ID" 0
|
|
681
702
|
if [ "${MULTI_AGENT_QUIET:-0}" != "1" ]; then
|
|
682
703
|
echo "phase-tracker: tracker initialized for task=$TASK_ID" >&2
|
|
683
704
|
echo "phase-tracker: TIP - for concurrent-safe resolution, set:" >&2
|
|
@@ -693,7 +714,7 @@ case "$ACTION" in
|
|
|
693
714
|
state=$(load_state)
|
|
694
715
|
# Idempotent: a phase id that already exists is a no-op (name, status, and
|
|
695
716
|
# token history are preserved). This is what lets continuation commands
|
|
696
|
-
# (/multi-agent:
|
|
717
|
+
# (/multi-agent:resume-local, resume) re-declare their phase set against a
|
|
697
718
|
# pre-existing tracker without duplicating or resetting tiles.
|
|
698
719
|
new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" '
|
|
699
720
|
if any(.phases[]?; .id == $id) then .
|
|
@@ -730,6 +751,7 @@ case "$ACTION" in
|
|
|
730
751
|
release_state_lock
|
|
731
752
|
emit_otel_span "phase.update" "$PID" "$STATUS" "{\"status\": \"$STATUS\"}"
|
|
732
753
|
render
|
|
754
|
+
usage_live_ping "$(basename "$TRACKER_DIR")" "$PID"
|
|
733
755
|
;;
|
|
734
756
|
|
|
735
757
|
sub)
|
|
@@ -766,9 +788,13 @@ case "$ACTION" in
|
|
|
766
788
|
[ "$#" -ge 3 ] || { echo "tokens needs <phase_id> <in> <out> [cached]" >&2; exit 64; }
|
|
767
789
|
PID="$1"; T_IN="$2"; T_OUT="$3"; T_CACHED="${4:-0}"
|
|
768
790
|
# Validate integers (cached is optional, defaults to 0 for back-compat).
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
791
|
+
# Each field checked separately: concatenation would let an empty field
|
|
792
|
+
# pass and feed jq --argjson an empty string, nuking the state file.
|
|
793
|
+
for v in "$T_IN" "$T_OUT" "$T_CACHED"; do
|
|
794
|
+
case "$v" in
|
|
795
|
+
''|*[!0-9]*) echo "tokens: in/out/cached must be non-negative integers" >&2; exit 64 ;;
|
|
796
|
+
esac
|
|
797
|
+
done
|
|
772
798
|
acquire_state_lock
|
|
773
799
|
state=$(load_state)
|
|
774
800
|
# Additive: add to existing totals so multiple LLM calls accumulate.
|
|
@@ -819,7 +845,8 @@ case "$ACTION" in
|
|
|
819
845
|
)')
|
|
820
846
|
save_state "$new"
|
|
821
847
|
release_state_lock
|
|
822
|
-
|
|
848
|
+
META_EXTRA=$(jq -nc --arg k "$KEY" --arg v "$VALUE" '{meta_key: $k, meta_value: $v}')
|
|
849
|
+
emit_otel_span "phase.meta" "$PID" "$KEY" "$META_EXTRA"
|
|
823
850
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
824
851
|
render
|
|
825
852
|
fi
|
|
@@ -76,7 +76,8 @@ export function inputTextOf(state) {
|
|
|
76
76
|
// evidence[].url / figmaFrames[] are where the analysis phase parks references.
|
|
77
77
|
for (const key of ["figmaFrames", "designRefs"]) {
|
|
78
78
|
const v = state[key];
|
|
79
|
-
if (Array.isArray(v))
|
|
79
|
+
if (Array.isArray(v))
|
|
80
|
+
parts.push(v.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(" "));
|
|
80
81
|
}
|
|
81
82
|
return parts.join("\n");
|
|
82
83
|
}
|
|
@@ -198,7 +199,7 @@ function main(argv) {
|
|
|
198
199
|
const args = argv.slice(2);
|
|
199
200
|
const taskId = args.find((a) => !a.startsWith("--"));
|
|
200
201
|
if (!taskId) {
|
|
201
|
-
console.error(
|
|
202
|
+
console.error('usage: phase0-exit-gate.mjs <task_id> [--input "<text>"] [--json]');
|
|
202
203
|
return 2;
|
|
203
204
|
}
|
|
204
205
|
const inputIdx = args.indexOf("--input");
|
|
@@ -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(
|
|
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]) => ({
|
|
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-*-
|
|
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(
|
|
500
|
-
|
|
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(
|
|
505
|
-
|
|
506
|
-
|
|
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(
|
|
510
|
-
|
|
511
|
-
|
|
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(
|
|
514
|
-
|
|
515
|
-
|
|
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(
|
|
519
|
-
|
|
520
|
-
|
|
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")
|
|
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({
|
|
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({
|
|
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 =
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
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
|
|
892
|
+
loadedAs: unscoped
|
|
893
|
+
? "reference-only"
|
|
894
|
+
: inScopeFiles.length
|
|
895
|
+
? "rule-checklist"
|
|
896
|
+
: "not-applicable",
|
|
780
897
|
...(unscoped
|
|
781
|
-
? {
|
|
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) =>
|
|
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:
|
|
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(
|
|
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(
|
|
944
|
-
|
|
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(
|
|
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-
|
|
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
|
+
}
|