@mmerterden/multi-agent-pipeline 14.2.1 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -6
- package/README.md +20 -9
- package/README.tr.md +150 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.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 +24 -9
- package/docs/ecosystem.md +237 -0
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +33 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +37 -7
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +15 -12
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
- 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 +13 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- 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 +1 -1
- package/pipeline/lib/extract-conventions.sh +44 -15
- package/pipeline/lib/fetch-figma-annotations.sh +8 -1
- package/pipeline/lib/fetch-fortify.sh +23 -8
- package/pipeline/lib/figma-screenshot.sh +11 -1
- package/pipeline/lib/issue-fetcher.sh +77 -10
- package/pipeline/lib/md2confluence-v3.py +16 -2
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/lib/plan-todos.sh +5 -2
- package/pipeline/lib/post-pr-review.sh +8 -6
- package/pipeline/lib/shadow-git.sh +50 -9
- package/pipeline/lib/submodule-detector.sh +8 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +13 -2
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +20 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
- 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/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +3 -3
- package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
- package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
- package/pipeline/multi-agent-refs/phases/modes.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
- 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-6-commit.md +8 -40
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
- package/pipeline/multi-agent-refs/phases.md +6 -0
- package/pipeline/multi-agent-refs/rules.md +2 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -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 +277 -67
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +14 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +35 -60
- package/pipeline/scripts/check-derived-drift.mjs +65 -29
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +33 -12
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/pre-commit-check.sh +69 -22
- package/pipeline/scripts/render-agent-log-cost.sh +8 -3
- package/pipeline/scripts/render-cost-summary.sh +42 -22
- package/pipeline/scripts/render-work-summary.sh +47 -13
- package/pipeline/scripts/review-scope.mjs +1 -1
- package/pipeline/scripts/run-aggregator.mjs +43 -14
- 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/smoke-schema-validation.sh +5 -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/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +196 -14
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +459 -13
- package/pipeline/skills/shared/README.md +15 -11
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
- 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-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -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/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/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
- package/pipeline/skills/skills-index.md +9 -5
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"warn_tokens": 5600
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
"total_max_tokens":
|
|
40
|
-
"note": "Token estimate = ceil(chars / 4). Per-phase budget rule: warn = current+10% (rounded to nearest 50), max = current+25%. Gives ~6 edit cycles of headroom before warn trips - intentionally quiet under normal maintenance, loud when a phase grows unusually. Only the active phase is loaded (lazy). Recalibrated at v10.0.0 after the validator/consistency/simplifier/lesson gate contracts landed in phases 1-4. Recalibrated again at v10.9.0 after the verify-by-test (Phase 4 Step 3.7), update-check (Phase 0 Step 0.6), immutable-test (Phase 3 GREEN) and redTests re-entry contracts landed - Step 3.7 prose was compressed to a pointer into refs/features/verify-by-test.md before the recalibration. Total bumped 50000 -> 51000 at v12.5.0 after the worktree residue/traversal-prune contract (Phase 0 + Phase 5 heal) and the Reflexion causal-diagnosis contract (Phase 4 lesson memory) landed; the prose was compressed first (161 tokens reclaimed) and every per-phase max still passes - only the aggregate needed room. Recalibrated again at v13.6.0 after the install-relative path correction: an instruction that names `pipeline/scripts/x` resolves only from a repo checkout, and a run happens in the user's worktree, so 157 references across these docs moved to `$HOME/.claude/...` at +5 bytes each - 196 tokens of pure correctness cost. Same discipline as before: prose was compressed FIRST (149 tokens reclaimed, by pointing Phase 1's Figma tier table at the Phase 0 probe that already resolved it and Phase 4's Codex constraints at the always-loaded AGENTS.md block), and only then were the budgets moved. Five warn lines had been permanently amber, which makes the amber tier useless as a signal, so every warn was reset to the documented current+10% and the four maxes that the new warn would have collided with were reset to current+25%. Aggregate 51000 -> 51500. Total bumped 51500 -> 52200 at v14.0.0 after Phase 4 Review entered the four --dev mode phase sets and the criteria-resolution contract (Step 1.78) landed. Same discipline as every prior bump: prose was compressed FIRST, 820 tokens reclaimed, before the number moved. Two of those compressions are structural rather than cosmetic - the hardcoded SwiftUI interaction list in Step 1.5 and the SwiftUI convention paragraph in Step 2.8 were transcriptions of rules that now live in a scoped registry, so keeping them here would have re-created the drift this release exists to remove, and the third moved the Step 1.78 full contract into refs/features/skill-conformance.md leaving a pointer. What remains is contract text that cannot be inferred: the manifest's four consumer-visible parts, the conformance checklist the reviewers must return, and the fail-closed semantics. Every per-phase max still passes (phase-4 12405/14750); only the aggregate needed room. Total bumped 52200 -> 52700 at v14.1.0 after two more contracts landed: stack skill routing (Phase 3 pre-flight step 9) and worktree finalize (Phase 6 step 9). Compression came first, as always, and twice: 224 tokens out of Phase 3 by pointing its criteria-ledger and routing steps at their feature files instead of restating them, and 190 out of Phase 6 by moving the finalize contract into refs/features/worktree-finalize.md and leaving the invocation plus the exit-3 semantics. Both new contracts follow the pattern the earlier ones set: the phase doc carries the call and the decision, the feature file carries the reasoning, and the feature files are outside this budget because it loops only the eight phase-N-* keys. Every per-phase max still passes (phase-3 7677/8950, phase-6 5223/6150 and both under warn); only the aggregate needed room. Total bumped 52700 -> 52750 for the Phase 0 Step 3 branch-persistence correction: the step wrote the legacy `projects[].branches` while the TTL filter two sections below read `global.recentBranches`, and both spots named a `{name, lastUsed}` shape the schema rejects (`branch` required, `additionalProperties: false`), so the recent-branch picker option could never populate and a literal implementation would have failed prefs validation. Naming the right target, the right key and the legacy field to avoid costs 41 tokens over the one line it replaces. Compression came first and was applied three times to the replacement text itself, from 120 tokens down to 66, by moving the rationale out of the phase doc entirely: the reasoning now lives where it is enforced, in the migrate-prefs carry-forward comment and the smoke-pref-migration f7 block, leaving the phase doc with only the instruction. 50 was the smallest step that clears it; phase-0-init sits at 10893/12400, far under its own max, so this is purely an aggregate ceiling."
|
|
39
|
+
"total_max_tokens": 53100,
|
|
40
|
+
"note": "Token estimate = ceil(chars / 4). Per-phase budget rule: warn = current+10% (rounded to nearest 50), max = current+25%. Gives ~6 edit cycles of headroom before warn trips - intentionally quiet under normal maintenance, loud when a phase grows unusually. Only the active phase is loaded (lazy). Recalibrated at v10.0.0 after the validator/consistency/simplifier/lesson gate contracts landed in phases 1-4. Recalibrated again at v10.9.0 after the verify-by-test (Phase 4 Step 3.7), update-check (Phase 0 Step 0.6), immutable-test (Phase 3 GREEN) and redTests re-entry contracts landed - Step 3.7 prose was compressed to a pointer into refs/features/verify-by-test.md before the recalibration. Total bumped 50000 -> 51000 at v12.5.0 after the worktree residue/traversal-prune contract (Phase 0 + Phase 5 heal) and the Reflexion causal-diagnosis contract (Phase 4 lesson memory) landed; the prose was compressed first (161 tokens reclaimed) and every per-phase max still passes - only the aggregate needed room. Recalibrated again at v13.6.0 after the install-relative path correction: an instruction that names `pipeline/scripts/x` resolves only from a repo checkout, and a run happens in the user's worktree, so 157 references across these docs moved to `$HOME/.claude/...` at +5 bytes each - 196 tokens of pure correctness cost. Same discipline as before: prose was compressed FIRST (149 tokens reclaimed, by pointing Phase 1's Figma tier table at the Phase 0 probe that already resolved it and Phase 4's Codex constraints at the always-loaded AGENTS.md block), and only then were the budgets moved. Five warn lines had been permanently amber, which makes the amber tier useless as a signal, so every warn was reset to the documented current+10% and the four maxes that the new warn would have collided with were reset to current+25%. Aggregate 51000 -> 51500. Total bumped 51500 -> 52200 at v14.0.0 after Phase 4 Review entered the four --dev mode phase sets and the criteria-resolution contract (Step 1.78) landed. Same discipline as every prior bump: prose was compressed FIRST, 820 tokens reclaimed, before the number moved. Two of those compressions are structural rather than cosmetic - the hardcoded SwiftUI interaction list in Step 1.5 and the SwiftUI convention paragraph in Step 2.8 were transcriptions of rules that now live in a scoped registry, so keeping them here would have re-created the drift this release exists to remove, and the third moved the Step 1.78 full contract into refs/features/skill-conformance.md leaving a pointer. What remains is contract text that cannot be inferred: the manifest's four consumer-visible parts, the conformance checklist the reviewers must return, and the fail-closed semantics. Every per-phase max still passes (phase-4 12405/14750); only the aggregate needed room. Total bumped 52200 -> 52700 at v14.1.0 after two more contracts landed: stack skill routing (Phase 3 pre-flight step 9) and worktree finalize (Phase 6 step 9). Compression came first, as always, and twice: 224 tokens out of Phase 3 by pointing its criteria-ledger and routing steps at their feature files instead of restating them, and 190 out of Phase 6 by moving the finalize contract into refs/features/worktree-finalize.md and leaving the invocation plus the exit-3 semantics. Both new contracts follow the pattern the earlier ones set: the phase doc carries the call and the decision, the feature file carries the reasoning, and the feature files are outside this budget because it loops only the eight phase-N-* keys. Every per-phase max still passes (phase-3 7677/8950, phase-6 5223/6150 and both under warn); only the aggregate needed room. Total bumped 52700 -> 52750 for the Phase 0 Step 3 branch-persistence correction: the step wrote the legacy `projects[].branches` while the TTL filter two sections below read `global.recentBranches`, and both spots named a `{name, lastUsed}` shape the schema rejects (`branch` required, `additionalProperties: false`), so the recent-branch picker option could never populate and a literal implementation would have failed prefs validation. Naming the right target, the right key and the legacy field to avoid costs 41 tokens over the one line it replaces. Compression came first and was applied three times to the replacement text itself, from 120 tokens down to 66, by moving the rationale out of the phase doc entirely: the reasoning now lives where it is enforced, in the migrate-prefs carry-forward comment and the smoke-pref-migration f7 block, leaving the phase doc with only the instruction. 50 was the smallest step that clears it; phase-0-init sits at 10893/12400, far under its own max, so this is purely an aggregate ceiling. v15.0.0: total 52750 -> 53100, the stack-skill tables in phase-1/2/4 now carry plugin-namespaced names (ai-<stack>-toolkit:<skill>) - functional prefixes, ~170 tokens."
|
|
41
41
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// _stack-routing.mjs
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for "which marketplace plugin carries this external
|
|
4
|
+
// skill". Extracted from build-stack-plugins.mjs so the skills-index builder can
|
|
5
|
+
// stamp a `plugin` field per entry without duplicating the routing table - a
|
|
6
|
+
// second copy would drift the first time a pattern changed, and the index would
|
|
7
|
+
// namespace skills into a plugin that no longer carries them.
|
|
8
|
+
//
|
|
9
|
+
// Routing order is load-bearing and mirrors build-stack-plugins step 2:
|
|
10
|
+
// COMMON_SKILLS first (explicit allowlist, wins over any pattern), then
|
|
11
|
+
// APPLE_ONLY (Apple/Xcode-only names no pattern catches), then STACK_PATTERNS.
|
|
12
|
+
// A skill may match several stack patterns; build-stack-plugins ships it to all
|
|
13
|
+
// of them and --check-routing polices accidental multi-routing. routeSkill()
|
|
14
|
+
// returns the full match list so callers can decide.
|
|
15
|
+
|
|
16
|
+
export const COMMON_PLUGIN = "ai-common-toolkit";
|
|
17
|
+
|
|
18
|
+
export const COMMON_SKILLS = [
|
|
19
|
+
"accessibility-compliance-accessibility-audit",
|
|
20
|
+
"firebase",
|
|
21
|
+
"humanizer",
|
|
22
|
+
"council",
|
|
23
|
+
"search-first",
|
|
24
|
+
"agent-introspection-debugging",
|
|
25
|
+
"skill-creator",
|
|
26
|
+
"backlog",
|
|
27
|
+
"localization-reuse-map",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Apple/Xcode-only skills that match no stack pattern → iOS plugin only.
|
|
31
|
+
export const APPLE_ONLY = [
|
|
32
|
+
"avkit",
|
|
33
|
+
"cryptokit",
|
|
34
|
+
"metrickit-diagnostics",
|
|
35
|
+
"pdfkit",
|
|
36
|
+
"spm-build-analysis",
|
|
37
|
+
"swiftlint",
|
|
38
|
+
"xcode-build-benchmark",
|
|
39
|
+
"xcode-build-fixer",
|
|
40
|
+
"xcode-build-orchestrator",
|
|
41
|
+
"xcode-compilation-analyzer",
|
|
42
|
+
"xcode-project-analyzer",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// stack → matching pattern (per-stack routing patterns (formerly in stack-swap.sh))
|
|
46
|
+
//
|
|
47
|
+
// A skill lands in every plugin whose pattern it matches, so a bare substring
|
|
48
|
+
// alternative that is also a suffix of another stack's skill name silently ships
|
|
49
|
+
// that skill twice. `architecture` did exactly that: it pulled
|
|
50
|
+
// `android-architecture` and `swift-architecture` into ai-backend-toolkit
|
|
51
|
+
// alongside their correct plugins, so a backend project loading the toolkit got
|
|
52
|
+
// Compose and SwiftUI architecture skills. It is anchored to the exact name now.
|
|
53
|
+
// Keep any new single-word alternative anchored unless it genuinely reads as a
|
|
54
|
+
// substring across stacks.
|
|
55
|
+
export const STACK_PATTERNS = {
|
|
56
|
+
"ai-ios-toolkit":
|
|
57
|
+
/(swiftui|swift-|ios-|hig-|apple-|storekit|widgetkit|healthkit|homekit|mapkit|musickit|passkit|pencilkit|realitykit|weatherkit|alarmkit|callkit|cloudkit|coreml|core-|eventkit|energykit|permissionkit|tipkit|shareplay|live-activities|background-processing|app-store|app-clips|app-intents|authentication|contacts-framework|device-integrity|macos-|natural-language|photos-camera|push-notifications|speech-recognition|swiftdata|vision-framework|debugging-instruments|help-skills)/i,
|
|
58
|
+
"ai-android-toolkit":
|
|
59
|
+
/(android|compose-|kotlin-|room-database|retrofit-|gradle-|play-store)/i,
|
|
60
|
+
"ai-backend-toolkit":
|
|
61
|
+
/(fastapi|nodejs|docker|api-pattern|api-security|github-actions|observability|^architecture$|monorepo|clean-code|debugging-strategies|agentflow|closed-loop|context-compression|python-patterns|database-patterns|rest-api-design|testing-backend|ci-cd-pipelines)/i,
|
|
62
|
+
"ai-frontend-toolkit":
|
|
63
|
+
/(react-|nextjs-|typescript-|tailwind-|vue-|web-accessibility|web-performance|css-modern|web-testing|html-semantic)/i,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which plugins carry this external skill, in routing order.
|
|
68
|
+
* @param {string} name - skill directory name
|
|
69
|
+
* @returns {string[]} plugin names; empty when unrouted
|
|
70
|
+
*/
|
|
71
|
+
export function routeSkill(name) {
|
|
72
|
+
if (COMMON_SKILLS.includes(name)) return [COMMON_PLUGIN];
|
|
73
|
+
if (APPLE_ONLY.includes(name)) return ["ai-ios-toolkit"];
|
|
74
|
+
const hits = [];
|
|
75
|
+
for (const [plugin, re] of Object.entries(STACK_PATTERNS)) {
|
|
76
|
+
if (re.test(name)) hits.push(plugin);
|
|
77
|
+
}
|
|
78
|
+
return hits;
|
|
79
|
+
}
|
|
@@ -57,7 +57,10 @@ if [ -s "$AUDIT_FILE" ]; then
|
|
|
57
57
|
# event appended between the copy and the truncate.
|
|
58
58
|
SPOOL="$AUDIT_DIR/.audit.rotate.$$"
|
|
59
59
|
mv "$AUDIT_FILE" "$SPOOL"
|
|
60
|
-
:
|
|
60
|
+
# touch, not truncate: a writer appending between the mv and this line
|
|
61
|
+
# recreates the live file, and `: >` would destroy that event. touch keeps
|
|
62
|
+
# whatever a racing writer already appended.
|
|
63
|
+
touch "$AUDIT_FILE"
|
|
61
64
|
chmod 600 "$AUDIT_FILE" 2>/dev/null || true
|
|
62
65
|
|
|
63
66
|
# Don't clobber a rotated file from earlier today (multiple invocations).
|
|
@@ -80,6 +83,16 @@ if [ -s "$AUDIT_FILE" ]; then
|
|
|
80
83
|
if [ -f "$ROTATED" ] && command -v gzip >/dev/null 2>&1; then
|
|
81
84
|
gzip -f "$ROTATED" 2>/dev/null || true
|
|
82
85
|
fi
|
|
86
|
+
|
|
87
|
+
# gzip preserves the source file's mtime by default, so an archive rotated
|
|
88
|
+
# from log content already older than KEEP_DAYS would carry that same old
|
|
89
|
+
# mtime - and the retention sweep below deletes anything past KEEP_DAYS,
|
|
90
|
+
# so a just-created archive could be purged in this very run before ever
|
|
91
|
+
# being read. Retention has to count from rotation time, not from the age
|
|
92
|
+
# of the content it archived. gzip -f removes the plain file on success, so
|
|
93
|
+
# only touch whichever of the two actually exists.
|
|
94
|
+
[ -f "$ROTATED.gz" ] && touch "$ROTATED.gz"
|
|
95
|
+
[ -f "$ROTATED" ] && touch "$ROTATED"
|
|
83
96
|
fi
|
|
84
97
|
|
|
85
98
|
# Drop archives older than KEEP_DAYS
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { dirname, join, relative, resolve } from "node:path";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { routeSkill } from "./_stack-routing.mjs";
|
|
29
30
|
|
|
30
31
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
const repoRoot = resolve(here, "..", "..");
|
|
@@ -92,11 +93,21 @@ for (const full of walkSkills(root)) {
|
|
|
92
93
|
.map((s) => s.trim())
|
|
93
94
|
.filter(Boolean);
|
|
94
95
|
|
|
96
|
+
// External skills reach Claude Code only through the marketplace plugins, so
|
|
97
|
+
// the index carries which plugin owns each one. `invokeAs` is the name a
|
|
98
|
+
// consumer must use: plugin-namespaced on Claude Code, bare elsewhere (the
|
|
99
|
+
// Copilot/Codex copies keep bare directory names). Routing comes from
|
|
100
|
+
// _stack-routing.mjs - the same table build-stack-plugins ships with.
|
|
101
|
+
const plugins = group === "external" ? routeSkill(name) : [];
|
|
102
|
+
const plugin = plugins[0] || null;
|
|
103
|
+
|
|
95
104
|
entries.push({
|
|
96
105
|
name,
|
|
97
106
|
description,
|
|
98
107
|
platform,
|
|
99
108
|
group,
|
|
109
|
+
plugin,
|
|
110
|
+
invokeAs: plugin ? `${plugin}:${name}` : name,
|
|
100
111
|
triggerKeywords: kws,
|
|
101
112
|
triggerPaths: paths,
|
|
102
113
|
relativePath: rel,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* knowledge entries are regenerated.
|
|
15
15
|
*
|
|
16
16
|
* Routing uses the per-stack skill patterns (formerly carried by stack-swap.sh). Genuinely cross-stack
|
|
17
|
-
* skills go to ai-common-
|
|
17
|
+
* skills go to ai-common-toolkit (not into each stack plugin). Apple/
|
|
18
18
|
* Xcode-only vendored skills that match no stack pattern belong to iOS only.
|
|
19
19
|
*
|
|
20
20
|
* Usage:
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
37
38
|
|
|
38
39
|
const args = process.argv.slice(2);
|
|
39
40
|
// Accepts both `--key value` and `--key=value`. The `=` form used to fall through to
|
|
@@ -51,53 +52,12 @@ const PIPE_ROOT = getArg("--pipeline", join(HOME, "multi-agent-pipeline"));
|
|
|
51
52
|
const EXTERNAL = join(PIPE_ROOT, "pipeline/skills/shared/external");
|
|
52
53
|
const DRY = args.includes("--dry-run");
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"search-first",
|
|
61
|
-
"agent-introspection-debugging",
|
|
62
|
-
"skill-creator",
|
|
63
|
-
"backlog",
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
// Apple/Xcode-only skills that match no stack pattern → iOS plugin only.
|
|
67
|
-
const APPLE_ONLY = [
|
|
68
|
-
"avkit",
|
|
69
|
-
"cryptokit",
|
|
70
|
-
"metrickit-diagnostics",
|
|
71
|
-
"pdfkit",
|
|
72
|
-
"spm-build-analysis",
|
|
73
|
-
"swiftlint",
|
|
74
|
-
"xcode-build-benchmark",
|
|
75
|
-
"xcode-build-fixer",
|
|
76
|
-
"xcode-build-orchestrator",
|
|
77
|
-
"xcode-compilation-analyzer",
|
|
78
|
-
"xcode-project-analyzer",
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// stack → matching pattern (per-stack routing patterns (formerly in stack-swap.sh))
|
|
82
|
-
//
|
|
83
|
-
// A skill lands in every plugin whose pattern it matches, so a bare substring
|
|
84
|
-
// alternative that is also a suffix of another stack's skill name silently ships
|
|
85
|
-
// that skill twice. `architecture` did exactly that: it pulled
|
|
86
|
-
// `android-architecture` and `swift-architecture` into ai-backend-toolkit
|
|
87
|
-
// alongside their correct plugins, so a backend project loading the toolkit got
|
|
88
|
-
// Compose and SwiftUI architecture skills. It is anchored to the exact name now.
|
|
89
|
-
// Keep any new single-word alternative anchored unless it genuinely reads as a
|
|
90
|
-
// substring across stacks.
|
|
91
|
-
const STACK_PATTERNS = {
|
|
92
|
-
"ai-ios-engineering-toolkit":
|
|
93
|
-
/(swiftui|swift-|ios-|hig-|apple-|storekit|widgetkit|healthkit|homekit|mapkit|musickit|passkit|pencilkit|realitykit|weatherkit|alarmkit|callkit|cloudkit|coreml|core-|eventkit|energykit|permissionkit|tipkit|shareplay|live-activities|background-processing|app-store|app-clips|app-intents|authentication|contacts-framework|device-integrity|macos-|natural-language|photos-camera|push-notifications|speech-recognition|swiftdata|vision-framework|debugging-instruments|help-skills)/i,
|
|
94
|
-
"ai-android-engineering-toolkit":
|
|
95
|
-
/(android|compose-|kotlin-|room-database|retrofit-|gradle-|play-store)/i,
|
|
96
|
-
"ai-backend-toolkit":
|
|
97
|
-
/(fastapi|nodejs|docker|api-pattern|api-security|github-actions|observability|^architecture$|monorepo|clean-code|debugging-strategies|agentflow|closed-loop|context-compression|python-patterns|database-patterns|rest-api-design|testing-backend|ci-cd-pipelines)/i,
|
|
98
|
-
"ai-frontend-engineering-toolkit":
|
|
99
|
-
/(react-|nextjs-|typescript-|tailwind-|vue-|web-accessibility|web-performance|css-modern|web-testing|html-semantic)/i,
|
|
100
|
-
};
|
|
55
|
+
import {
|
|
56
|
+
COMMON_PLUGIN,
|
|
57
|
+
COMMON_SKILLS,
|
|
58
|
+
APPLE_ONLY,
|
|
59
|
+
STACK_PATTERNS,
|
|
60
|
+
} from "./_stack-routing.mjs";
|
|
101
61
|
|
|
102
62
|
const isDir = (p) => {
|
|
103
63
|
try {
|
|
@@ -109,18 +69,23 @@ const isDir = (p) => {
|
|
|
109
69
|
const listDirs = (p) => (existsSync(p) ? readdirSync(p).filter((n) => isDir(join(p, n))) : []);
|
|
110
70
|
|
|
111
71
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
72
|
+
* Content fingerprint for a skill directory: every file's relative path and a
|
|
73
|
+
* hash of its bytes. A size-only fingerprint missed same-length edits, so a
|
|
74
|
+
* content update could ship without a version bump. The whole authoring tree
|
|
75
|
+
* is a few MB of markdown; hashing it per run costs milliseconds and is only
|
|
76
|
+
* used to decide whether a copy + version bump is warranted.
|
|
115
77
|
*/
|
|
116
78
|
function treeFingerprint(dir) {
|
|
117
79
|
const parts = [];
|
|
118
80
|
const walk = (d, rel) => {
|
|
119
|
-
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) =>
|
|
81
|
+
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) =>
|
|
82
|
+
a.name.localeCompare(b.name),
|
|
83
|
+
)) {
|
|
120
84
|
const full = join(d, e.name);
|
|
121
85
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
122
86
|
if (e.isDirectory()) walk(full, r);
|
|
123
|
-
else if (e.isFile())
|
|
87
|
+
else if (e.isFile())
|
|
88
|
+
parts.push(`${r}:${createHash("sha1").update(readFileSync(full)).digest("hex")}`);
|
|
124
89
|
}
|
|
125
90
|
};
|
|
126
91
|
try {
|
|
@@ -134,7 +99,13 @@ function treeFingerprint(dir) {
|
|
|
134
99
|
// --- 1. enumerate authoring source ---------------------------------------
|
|
135
100
|
if (!existsSync(EXTERNAL)) {
|
|
136
101
|
console.error(`source not found: ${EXTERNAL}`);
|
|
137
|
-
|
|
102
|
+
// --check-routing exists so CI can gate routing correctness without a
|
|
103
|
+
// checkout of the plugins repo, reading only this authoring source. Exiting
|
|
104
|
+
// 0 here regardless of --check-routing meant a misconfigured/missing
|
|
105
|
+
// source (wrong --pipeline path, an incomplete CI checkout) reported the
|
|
106
|
+
// gate as passing having audited nothing - the exact silent-green failure
|
|
107
|
+
// the gate exists to prevent elsewhere.
|
|
108
|
+
process.exit(args.includes("--check-routing") ? 1 : 0);
|
|
138
109
|
}
|
|
139
110
|
const allSkills = listDirs(EXTERNAL);
|
|
140
111
|
|
|
@@ -149,7 +120,7 @@ for (const skill of allSkills) {
|
|
|
149
120
|
continue;
|
|
150
121
|
}
|
|
151
122
|
if (APPLE_ONLY.includes(skill)) {
|
|
152
|
-
desired["ai-ios-
|
|
123
|
+
desired["ai-ios-toolkit"].add(skill);
|
|
153
124
|
continue;
|
|
154
125
|
}
|
|
155
126
|
let matched = false;
|
|
@@ -211,7 +182,8 @@ for (const [plugin, want] of Object.entries(desired)) {
|
|
|
211
182
|
const toRemove = [...have].filter((s) => !want.has(s)).sort();
|
|
212
183
|
const setChanged = toAdd.length > 0 || toRemove.length > 0;
|
|
213
184
|
|
|
214
|
-
//
|
|
185
|
+
// Compare EVERY wanted skill against the source, not just the newly added
|
|
186
|
+
// ones, and replace (rm + copy) rather than overlay.
|
|
215
187
|
//
|
|
216
188
|
// This used to copy only on a set change, which quietly broke the single-source
|
|
217
189
|
// claim: editing a skill in shared/external left the plugin serving the old text
|
|
@@ -219,17 +191,20 @@ for (const [plugin, want] of Object.entries(desired)) {
|
|
|
219
191
|
// seeing the plugin copy stay stale while the generator reported "all plugins up
|
|
220
192
|
// to date".
|
|
221
193
|
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
194
|
+
// The rm-before-copy matters too: cpSync overlays and never deletes, so a file
|
|
195
|
+
// REMOVED inside a still-wanted skill survived in every plugin copy forever -
|
|
196
|
+
// and because the overlay kept the orphan on both sides of the fingerprint
|
|
197
|
+
// comparison, the version was not bumped either.
|
|
224
198
|
let contentChanged = false;
|
|
225
199
|
if (!DRY) {
|
|
226
200
|
if (!existsSync(kdir)) mkdirSync(kdir, { recursive: true });
|
|
227
201
|
for (const s of want) {
|
|
228
202
|
const from = join(EXTERNAL, s);
|
|
229
203
|
const to = join(kdir, s);
|
|
230
|
-
|
|
204
|
+
if (treeFingerprint(from) === (existsSync(to) ? treeFingerprint(to) : "")) continue;
|
|
205
|
+
rmSync(to, { recursive: true, force: true });
|
|
231
206
|
cpSync(from, to, { recursive: true });
|
|
232
|
-
|
|
207
|
+
contentChanged = true;
|
|
233
208
|
}
|
|
234
209
|
for (const s of toRemove) rmSync(join(kdir, s), { recursive: true, force: true });
|
|
235
210
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { Buffer } from "node:buffer";
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
|
|
@@ -38,6 +38,18 @@ const CACHE = process.env.DERIVED_DRIFT_CACHE || join(homedir(), ".claude", "plu
|
|
|
38
38
|
|
|
39
39
|
const expandHome = (p) => p.replace(/^\$HOME\b|^~/, homedir());
|
|
40
40
|
|
|
41
|
+
// Array.prototype.sort() with no comparator sorts these lexicographically
|
|
42
|
+
// ("10.0.0" < "9.0.0" as strings), so a directory listing with both would
|
|
43
|
+
// treat 9.0.0 as newer.
|
|
44
|
+
function semverCompare(a, b) {
|
|
45
|
+
const pa = a.split(".").map(Number);
|
|
46
|
+
const pb = b.split(".").map(Number);
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
function readJson(path) {
|
|
42
54
|
try {
|
|
43
55
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -60,29 +72,44 @@ function versionFrom(manifest, pluginName) {
|
|
|
60
72
|
function fromLocalClone(entry) {
|
|
61
73
|
if (!entry.upstreamLocalClone) return null;
|
|
62
74
|
const root = expandHome(entry.upstreamLocalClone);
|
|
63
|
-
const
|
|
75
|
+
const relFile =
|
|
64
76
|
entry.upstreamVersionSource === "plugin.json"
|
|
65
|
-
?
|
|
66
|
-
:
|
|
67
|
-
if (!existsSync(file)) return null;
|
|
68
|
-
const version = versionFrom(readJson(file), entry.upstreamPlugin);
|
|
69
|
-
if (!version) return null;
|
|
77
|
+
? `plugins/${entry.upstreamPlugin}/.claude-plugin/plugin.json`
|
|
78
|
+
: ".claude-plugin/marketplace.json";
|
|
70
79
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
80
|
+
// Read the manifest from origin/main, never the checked-out worktree: a
|
|
81
|
+
// clone parked on an old feature branch reported stale versions as current,
|
|
82
|
+
// flagging correctly-bumped derivations as drifted.
|
|
74
83
|
try {
|
|
75
|
-
const
|
|
84
|
+
const raw = execFileSync("git", ["-C", root, "show", `origin/main:${relFile}`], {
|
|
76
85
|
encoding: "utf-8",
|
|
77
86
|
stdio: ["ignore", "pipe", "ignore"],
|
|
78
|
-
})
|
|
79
|
-
|
|
87
|
+
});
|
|
88
|
+
const version = versionFrom(JSON.parse(raw), entry.upstreamPlugin);
|
|
89
|
+
if (version) {
|
|
90
|
+
// origin/main is only as fresh as the last fetch. Surface fetch age so a
|
|
91
|
+
// months-old ref is not silently trusted.
|
|
92
|
+
let fetchAgeDays = null;
|
|
93
|
+
try {
|
|
94
|
+
const fetchHead = join(root, ".git", "FETCH_HEAD");
|
|
95
|
+
if (existsSync(fetchHead)) {
|
|
96
|
+
fetchAgeDays = Math.floor((Date.now() - statSync(fetchHead).mtimeMs) / 86_400_000);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
fetchAgeDays = null;
|
|
100
|
+
}
|
|
101
|
+
return { version, source: "local clone origin/main", authoritative: true, fetchAgeDays };
|
|
102
|
+
}
|
|
80
103
|
} catch {
|
|
81
|
-
// No
|
|
82
|
-
|
|
104
|
+
// No origin/main ref (not a git checkout, or unfetched): fall through to
|
|
105
|
+
// the worktree file, which is better than nothing but never fresher.
|
|
83
106
|
}
|
|
84
107
|
|
|
85
|
-
|
|
108
|
+
const file = join(root, relFile);
|
|
109
|
+
if (!existsSync(file)) return null;
|
|
110
|
+
const version = versionFrom(readJson(file), entry.upstreamPlugin);
|
|
111
|
+
if (!version) return null;
|
|
112
|
+
return { version, source: "local clone worktree", authoritative: true, fetchAgeDays: null };
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
function fromRepoApi(entry) {
|
|
@@ -116,7 +143,7 @@ function fromCache(entry) {
|
|
|
116
143
|
if (!existsSync(dir)) return null;
|
|
117
144
|
const versions = readdirSync(dir)
|
|
118
145
|
.filter((d) => /^\d+\.\d+\.\d+$/.test(d))
|
|
119
|
-
.sort();
|
|
146
|
+
.sort(semverCompare);
|
|
120
147
|
const newest = versions[versions.length - 1];
|
|
121
148
|
return newest
|
|
122
149
|
? { version: newest, source: "plugin cache", authoritative: false, behind: null }
|
|
@@ -149,46 +176,55 @@ for (const entry of entries) {
|
|
|
149
176
|
continue;
|
|
150
177
|
}
|
|
151
178
|
const drifted = resolved.version !== entry.derivedFromVersion;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
179
|
+
// An acknowledged drift is a conscious "port pending" decision, pinned to the
|
|
180
|
+
// upstream version it was made against: it stays visible but does not fail
|
|
181
|
+
// the gate, and the moment upstream moves past the pin it is plain drift
|
|
182
|
+
// again.
|
|
183
|
+
const acknowledged = drifted && entry.driftAcknowledged === resolved.version;
|
|
184
|
+
const status = acknowledged
|
|
185
|
+
? "drift (acknowledged)"
|
|
186
|
+
: drifted
|
|
187
|
+
? "drift"
|
|
188
|
+
: resolved.authoritative
|
|
189
|
+
? "up to date"
|
|
190
|
+
: "unverified (cache only)";
|
|
157
191
|
rows.push({
|
|
158
192
|
label: entry.label,
|
|
159
193
|
plugin: entry.upstreamPlugin,
|
|
160
194
|
from: entry.derivedFromVersion,
|
|
161
195
|
upstream: resolved.version,
|
|
162
196
|
source: resolved.source,
|
|
163
|
-
|
|
197
|
+
fetchAgeDays: resolved.fetchAgeDays ?? null,
|
|
164
198
|
status,
|
|
165
199
|
});
|
|
166
200
|
}
|
|
167
201
|
|
|
168
202
|
const drifted = rows.filter((r) => r.status === "drift").length;
|
|
203
|
+
const acknowledged = rows.filter((r) => r.status === "drift (acknowledged)").length;
|
|
169
204
|
const unverified = rows.filter((r) => r.status.startsWith("unverified")).length;
|
|
170
205
|
const unreachable = rows.filter((r) => r.status === "unreachable").length;
|
|
171
206
|
|
|
172
207
|
if (asJson) {
|
|
173
208
|
console.log(
|
|
174
209
|
JSON.stringify(
|
|
175
|
-
{ configured: rows.length, drifted, unverified, unreachable, entries: rows },
|
|
210
|
+
{ configured: rows.length, drifted, acknowledged, unverified, unreachable, entries: rows },
|
|
176
211
|
null,
|
|
177
212
|
2,
|
|
178
213
|
),
|
|
179
214
|
);
|
|
180
215
|
} else {
|
|
181
216
|
for (const r of rows) {
|
|
182
|
-
const
|
|
217
|
+
const age =
|
|
218
|
+
r.fetchAgeDays != null && r.fetchAgeDays > 7 ? ` [last fetch ${r.fetchAgeDays}d ago]` : "";
|
|
183
219
|
console.log(
|
|
184
|
-
` ${r.status === "up to date" ? "✓" : "✗"} ${r.plugin}: recorded ${r.from}, ` +
|
|
185
|
-
`upstream ${r.upstream ?? "?"} (${r.source})${
|
|
220
|
+
` ${r.status === "up to date" ? "✓" : r.status === "drift (acknowledged)" ? "!" : "✗"} ${r.plugin}: recorded ${r.from}, ` +
|
|
221
|
+
`upstream ${r.upstream ?? "?"} (${r.source})${age} -> ${r.status}`,
|
|
186
222
|
);
|
|
187
223
|
}
|
|
188
224
|
console.log("");
|
|
189
225
|
console.log(
|
|
190
|
-
`══ check-derived-drift: ${rows.length - drifted - unverified - unreachable} passed
|
|
191
|
-
|
|
226
|
+
`══ check-derived-drift: ${rows.length - drifted - unverified - unreachable} passed ` +
|
|
227
|
+
`(${acknowledged} acknowledged), ${drifted + unverified + unreachable} failed ══`,
|
|
192
228
|
);
|
|
193
229
|
}
|
|
194
230
|
|
|
@@ -117,6 +117,44 @@ function resolveBase() {
|
|
|
117
117
|
die("no base ref found (tried origin/main, main, origin/master, master) - pass --base");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// git quotes a `diff --git` path (core.quotePath, on by default) whenever it
|
|
121
|
+
// has a non-ASCII byte or other unusual character - e.g. a Turkish filename
|
|
122
|
+
// renders as `diff --git "a/\303\226deme.swift" "b/\303\226deme.swift"`, not
|
|
123
|
+
// the bare a/...b/... form. The plain regex this replaced never matched that
|
|
124
|
+
// line at all, so every non-ASCII-named file dropped out of the explanation.
|
|
125
|
+
const DIFF_GIT_QUOTED_RE = /^diff --git "a\/((?:[^"\\]|\\.)*)" "b\/((?:[^"\\]|\\.)*)"$/;
|
|
126
|
+
const DIFF_GIT_UNQUOTED_RE = /^diff --git a\/(.+?) b\/(.+)$/;
|
|
127
|
+
|
|
128
|
+
function unquoteGitPath(s) {
|
|
129
|
+
const bytes = [];
|
|
130
|
+
for (let i = 0; i < s.length; i++) {
|
|
131
|
+
if (s[i] === "\\") {
|
|
132
|
+
const octal = s.slice(i + 1, i + 4);
|
|
133
|
+
if (/^[0-7]{3}$/.test(octal)) {
|
|
134
|
+
bytes.push(parseInt(octal, 8));
|
|
135
|
+
i += 3;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const simple = { "\\": 92, '"': 34, t: 9, n: 10, r: 13 };
|
|
139
|
+
const next = s[i + 1];
|
|
140
|
+
if (next in simple) {
|
|
141
|
+
bytes.push(simple[next]);
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
bytes.push(s.charCodeAt(i));
|
|
147
|
+
}
|
|
148
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function matchDiffGitHeader(line) {
|
|
152
|
+
const q = line.match(DIFF_GIT_QUOTED_RE);
|
|
153
|
+
if (q) return unquoteGitPath(q[2]);
|
|
154
|
+
const u = line.match(DIFF_GIT_UNQUOTED_RE);
|
|
155
|
+
return u ? u[2] : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
120
158
|
/**
|
|
121
159
|
* Parse unified diff into a map of {path: [{ oldStart, newStart, lines: [...] }]}
|
|
122
160
|
* Each hunk's `lines` retains the leading +/- /space prefix.
|
|
@@ -127,9 +165,9 @@ function parseDiff(raw) {
|
|
|
127
165
|
let currentHunks = null;
|
|
128
166
|
let currentHunk = null;
|
|
129
167
|
for (const line of raw.split("\n")) {
|
|
130
|
-
const fileMatch =
|
|
131
|
-
if (fileMatch) {
|
|
132
|
-
currentPath = fileMatch
|
|
168
|
+
const fileMatch = matchDiffGitHeader(line);
|
|
169
|
+
if (fileMatch !== null) {
|
|
170
|
+
currentPath = fileMatch;
|
|
133
171
|
currentHunks = [];
|
|
134
172
|
files[currentPath] = currentHunks;
|
|
135
173
|
currentHunk = null;
|