@mmerterden/multi-agent-pipeline 12.6.0 → 12.7.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 +83 -0
- package/README.md +18 -18
- package/docs/FIGMA_PIPELINE.md +34 -34
- package/docs/adr/0001-three-model-triage.md +12 -12
- package/docs/adr/0002-instruction-driven-flag.md +5 -5
- package/docs/adr/0003-unified-shared-skills.md +5 -5
- package/docs/adr/0004-zero-dependency-philosophy.md +5 -5
- package/docs/adr/0005-lazy-phase-docs.md +2 -2
- package/docs/adr/0006-skills-core-external-split.md +6 -6
- package/docs/adr/0007-multi-tool-adapter-framework.md +19 -19
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +19 -19
- package/docs/adr/README.md +1 -1
- package/docs/best-practices.md +3 -3
- package/docs/features.md +28 -28
- package/docs/performance.md +16 -16
- package/docs/recovery-guide.md +39 -39
- package/index.js +4 -4
- package/install/_common.mjs +5 -11
- package/install/_copilot-instructions.mjs +2 -2
- package/install/_dev-only-files.mjs +1 -1
- package/install/_platform-filter.mjs +1 -1
- package/install/_telemetry.mjs +1 -1
- package/install/claude.mjs +10 -9
- package/install/copilot.mjs +10 -19
- package/install/index.mjs +7 -15
- package/install/templates/copilot-instructions.md +54 -54
- package/install.js +1 -1
- package/package.json +13 -10
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/design-check/SKILL.md +1 -1
- 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/diff-explain/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/finish/SKILL.md +6 -6
- package/pipeline/commands/multi-agent/forget/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/purge/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/refactor/SKILL.md +16 -8
- package/pipeline/commands/multi-agent/resume/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/routines/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/save/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
- package/pipeline/eval/run-metrics-fixture.json +60 -13
- package/pipeline/lib/account-resolver.sh +1 -1
- package/pipeline/lib/channels-multi-repo.sh +1 -1
- package/pipeline/lib/context-link-extractor.sh +1 -1
- package/pipeline/lib/credential-store.sh +1 -1
- package/pipeline/lib/fetch-confluence.sh +1 -1
- package/pipeline/lib/fetch-crashlytics.sh +1 -1
- package/pipeline/lib/fetch-fortify.sh +1 -1
- package/pipeline/lib/fetch-graylog.sh +1 -1
- package/pipeline/lib/fetch-swagger.sh +1 -1
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/multi-repo-pipeline.sh +1 -1
- package/pipeline/lib/repo-cache.sh +1 -1
- package/pipeline/lib/submodule-detector.sh +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/component-generation.md +121 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +1 -1
- package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
- package/pipeline/preferences-template.json +5 -11
- package/pipeline/schemas/agent-state.schema.json +39 -9
- package/pipeline/schemas/analysis-output.schema.json +18 -4
- package/pipeline/schemas/analysis-spec.schema.json +120 -32
- package/pipeline/schemas/clarify-output.schema.json +15 -5
- package/pipeline/schemas/design-check-config.schema.json +32 -11
- package/pipeline/schemas/dev-critic-output.schema.json +20 -5
- package/pipeline/schemas/figma-project-config.schema.json +42 -10
- package/pipeline/schemas/learnings-ledger.schema.json +10 -2
- package/pipeline/schemas/migrations/figma-config-1.0.0-to-2.0.0.mjs +1 -4
- package/pipeline/schemas/migrations/prefs-2.0.0-to-2.1.0.mjs +24 -7
- package/pipeline/schemas/plan-todos.schema.json +6 -3
- package/pipeline/schemas/planning-output.schema.json +5 -1
- package/pipeline/schemas/prefs.schema.json +91 -229
- package/pipeline/schemas/test-gap.schema.json +5 -5
- package/pipeline/schemas/token-budget.json +8 -8
- package/pipeline/schemas/triage-corpus.schema.json +1 -1
- package/pipeline/scripts/aggregate-metrics.mjs +18 -6
- package/pipeline/scripts/build-skills-index.mjs +6 -2
- package/pipeline/scripts/build-stack-plugins.mjs +142 -39
- package/pipeline/scripts/check-derived-drift.mjs +196 -0
- package/pipeline/scripts/check-md-links.mjs +6 -2
- package/pipeline/scripts/classify-plan-safety.mjs +20 -7
- package/pipeline/scripts/cost-budget-check.mjs +2 -1
- package/pipeline/scripts/cost-table.json +1 -1
- package/pipeline/scripts/diff-explain.mjs +7 -3
- package/pipeline/scripts/diff-risk-score.mjs +13 -3
- package/pipeline/scripts/eval-golden-tasks-live.mjs +8 -3
- package/pipeline/scripts/eval-golden-tasks.mjs +21 -9
- package/pipeline/scripts/eval-intent.mjs +8 -4
- package/pipeline/scripts/eval-mine-corpus.mjs +14 -4
- package/pipeline/scripts/evidence-gate.mjs +7 -2
- package/pipeline/scripts/fixtures/install-layout.tsv +3 -3
- package/pipeline/scripts/gen-mode-dispatch.mjs +38 -21
- package/pipeline/scripts/gen-skills-index.mjs +18 -3
- package/pipeline/scripts/learning-curve.mjs +13 -3
- package/pipeline/scripts/learnings-ledger.mjs +103 -36
- package/pipeline/scripts/lint-mcp-refs.mjs +13 -2
- package/pipeline/scripts/lint-skills.mjs +20 -9
- package/pipeline/scripts/localize-commands.mjs +6 -1
- package/pipeline/scripts/match-skills.mjs +15 -4
- package/pipeline/scripts/migrate-prefs.mjs +33 -16
- package/pipeline/scripts/phase-tracker.sh +3 -1
- package/pipeline/scripts/repo-map.mjs +110 -64
- package/pipeline/scripts/review-scope.mjs +7 -1
- package/pipeline/scripts/routine-registry.mjs +4 -9
- package/pipeline/scripts/run-aggregator.mjs +11 -5
- package/pipeline/scripts/run-metrics.mjs +13 -8
- package/pipeline/scripts/run-smokes.mjs +57 -3
- package/pipeline/scripts/scorecard.mjs +258 -0
- package/pipeline/scripts/smoke-context-budget.sh +72 -0
- package/pipeline/scripts/smoke-model-fallback.sh +1 -1
- package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +86 -7
- package/pipeline/scripts/smoke-own-punctuation.sh +103 -0
- package/pipeline/scripts/smoke-workflow-audit.sh +43 -11
- package/pipeline/scripts/smoke-write-state.sh +49 -5
- package/pipeline/scripts/test-gap-rules/android.json +11 -11
- package/pipeline/scripts/test-gap-rules/ios.json +16 -11
- package/pipeline/scripts/test-gap-rules/node.json +19 -7
- package/pipeline/scripts/test-gap-rules/python.json +10 -4
- package/pipeline/scripts/test-gap-scan.mjs +44 -12
- package/pipeline/scripts/test-integrity-gate.mjs +5 -1
- package/pipeline/scripts/token-budget-report.mjs +44 -21
- package/pipeline/scripts/triage-memory.mjs +126 -34
- package/pipeline/scripts/uninstall.mjs +74 -30
- package/pipeline/scripts/validate-analysis-doc.mjs +15 -5
- package/pipeline/scripts/validate-diff-risk.mjs +32 -18
- package/pipeline/scripts/validate-test-gap.mjs +17 -7
- package/pipeline/scripts/validate-triage.mjs +17 -5
- package/pipeline/scripts/write-state.mjs +32 -9
- package/pipeline/skills/.skills-index.json +91 -91
- package/pipeline/skills/shared/README.md +57 -57
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +1 -1
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +26 -279
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +1 -1
- 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-diff-explain/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-finish/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-garbage-collect/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-kill/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-log/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +16 -8
- package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
- package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +4 -4
- package/pipeline/skills/shared/external/agentflow/SKILL.md +1 -1
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +1 -1
- package/pipeline/skills/shared/external/android_ui_verification/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +1 -1
- package/pipeline/skills/shared/external/app-store-changelog/SKILL.md +1 -1
- package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +12 -12
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -1
- package/pipeline/skills/shared/external/context-compression/SKILL.md +1 -1
- package/pipeline/skills/shared/external/council/SKILL.md +3 -3
- package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -1
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +1 -1
- package/pipeline/skills/shared/external/docker-expert/SKILL.md +1 -1
- package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +1 -1
- package/pipeline/skills/shared/external/firebase/SKILL.md +1 -1
- package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +1 -1
- package/pipeline/skills/shared/external/help-skills/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-components-content/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-components-status/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-components-system/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-foundations/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-inputs/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-platforms/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-technologies/SKILL.md +1 -1
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -1
- package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-debugger-agent/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-developer/SKILL.md +1 -1
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +1 -1
- package/pipeline/skills/shared/external/macos-menubar-tuist-app/SKILL.md +1 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +1 -1
- package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +1 -1
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -1
- package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/observability-engineer/SKILL.md +1 -1
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -1
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -1
- package/pipeline/skills/shared/external/search-first/SKILL.md +2 -2
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +12 -12
- package/pipeline/skills/shared/external/skill-creator/audit.md +21 -21
- package/pipeline/skills/shared/external/skill-creator/checklist.md +3 -3
- package/pipeline/skills/shared/external/skill-creator/examples.md +10 -10
- package/pipeline/skills/shared/external/skill-creator/label-check.md +17 -17
- package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +86 -50
- package/pipeline/skills/shared/external/skill-creator/template.md +9 -9
- package/pipeline/skills/shared/external/swift-concurrency-expert/SKILL.md +1 -1
- package/pipeline/skills/shared/external/swiftui-performance-audit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/swiftui-ui-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/swiftui-view-refactor/SKILL.md +1 -1
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -1
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -1
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -1
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -1
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -1
- package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -1
- package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -1
- package/pipeline/skills/shared/external/xcode-build-benchmark/schemas/build-benchmark.schema.json +9 -49
- package/pipeline/skills/skills-index.md +57 -57
- package/pipeline/scripts/smoke-plugin-validate.sh +0 -64
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scorecard.mjs - the measurable part of the review score, as a gate.
|
|
3
|
+
//
|
|
4
|
+
// A refactor review scores ten categories out of ten. Six of those numbers can
|
|
5
|
+
// be measured; four cannot, and writing a number for those is how a scorecard
|
|
6
|
+
// turns into self-congratulation. So this reports exactly two things:
|
|
7
|
+
//
|
|
8
|
+
// MEASURED a metric with a threshold, pass or fail
|
|
9
|
+
// UNMEASURED a category no script can score, named as such
|
|
10
|
+
//
|
|
11
|
+
// Nothing here re-implements a check. Each metric either reads a committed
|
|
12
|
+
// threshold file or shells out to the gate that already owns the question, so
|
|
13
|
+
// there is one source of truth per question and the scorecard cannot drift from
|
|
14
|
+
// the gates it summarises.
|
|
15
|
+
//
|
|
16
|
+
// Thresholds are locked at the values measured when the gate was written. They
|
|
17
|
+
// are a ratchet: lowering one is a deliberate, reviewable edit, and a metric
|
|
18
|
+
// that improves should have its floor raised.
|
|
19
|
+
//
|
|
20
|
+
// Runs on a clean clone: no $HOME lookups, no credentials, no network. The one
|
|
21
|
+
// metric that needs local configuration (derived-skill drift) reports
|
|
22
|
+
// NOT CONFIGURED rather than passing by default.
|
|
23
|
+
//
|
|
24
|
+
// Usage: node pipeline/scripts/scorecard.mjs [--json]
|
|
25
|
+
// Exit: 0 every measured metric within threshold, 1 otherwise
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
33
|
+
const asJson = process.argv.includes("--json");
|
|
34
|
+
|
|
35
|
+
// Locked floors. Raise when a metric improves; lowering one is the reviewable act.
|
|
36
|
+
const FLOORS = {
|
|
37
|
+
coverageLines: 72,
|
|
38
|
+
coverageBranches: 68,
|
|
39
|
+
coverageFunctions: 85,
|
|
40
|
+
contextBytes: 60000,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const results = [];
|
|
44
|
+
|
|
45
|
+
function measured(category, metric, ok, detail) {
|
|
46
|
+
results.push({ kind: "MEASURED", category, metric, ok, detail });
|
|
47
|
+
}
|
|
48
|
+
function unmeasured(category, why) {
|
|
49
|
+
results.push({ kind: "UNMEASURED", category, why });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Run a gate and report whether it passed. The gate owns the assertion; this
|
|
53
|
+
// only records the verdict, so the two can never disagree.
|
|
54
|
+
function gate(category, metric, argv) {
|
|
55
|
+
try {
|
|
56
|
+
execFileSync(argv[0], argv.slice(1), { cwd: ROOT, stdio: "pipe" });
|
|
57
|
+
return measured(category, metric, true, "gate passed");
|
|
58
|
+
} catch (e) {
|
|
59
|
+
const out = `${e.stdout || ""}${e.stderr || ""}`.trim().split("\n").slice(-3).join(" | ");
|
|
60
|
+
return measured(category, metric, false, out || `exit ${e.status}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Tests: the coverage floors must not be lowered ------------------------
|
|
65
|
+
// c8 enforces the numbers on every run; what nothing enforced was the numbers
|
|
66
|
+
// themselves. .c8rc.json reported 83% while the real figure over its own include
|
|
67
|
+
// set was 33%, because `all` was missing - so the floors are worth pinning.
|
|
68
|
+
{
|
|
69
|
+
const c8 = JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf-8"));
|
|
70
|
+
const checks = [
|
|
71
|
+
["lines", c8.lines, FLOORS.coverageLines],
|
|
72
|
+
["branches", c8.branches, FLOORS.coverageBranches],
|
|
73
|
+
["functions", c8.functions, FLOORS.coverageFunctions],
|
|
74
|
+
];
|
|
75
|
+
const low = checks.filter(([, actual, floor]) => !(actual >= floor));
|
|
76
|
+
measured(
|
|
77
|
+
"Tests",
|
|
78
|
+
"coverage floors held",
|
|
79
|
+
c8.all === true && low.length === 0,
|
|
80
|
+
c8.all !== true
|
|
81
|
+
? "`all` is not set, so the floors only cover files the tests already load"
|
|
82
|
+
: low.length
|
|
83
|
+
? low.map(([k, a, f]) => `${k} ${a} below the locked ${f}`).join(", ")
|
|
84
|
+
: checks.map(([k, a]) => `${k} ${a}`).join(", "),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Dependency management: no high or critical advisories ------------------
|
|
89
|
+
{
|
|
90
|
+
let vulns;
|
|
91
|
+
try {
|
|
92
|
+
const raw = execFileSync("npm", ["audit", "--json"], { cwd: ROOT, encoding: "utf-8" });
|
|
93
|
+
vulns = JSON.parse(raw).metadata.vulnerabilities;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// npm audit exits non-zero when it finds something; the JSON is still on stdout.
|
|
96
|
+
try {
|
|
97
|
+
vulns = JSON.parse(e.stdout).metadata.vulnerabilities;
|
|
98
|
+
} catch {
|
|
99
|
+
vulns = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!vulns) {
|
|
103
|
+
measured(
|
|
104
|
+
"Dependency management",
|
|
105
|
+
"no high or critical advisories",
|
|
106
|
+
false,
|
|
107
|
+
"npm audit produced no parseable report",
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
const bad = vulns.high + vulns.critical;
|
|
111
|
+
measured(
|
|
112
|
+
"Dependency management",
|
|
113
|
+
"no high or critical advisories",
|
|
114
|
+
bad === 0,
|
|
115
|
+
`critical ${vulns.critical}, high ${vulns.high}, moderate ${vulns.moderate}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Performance: the bytes every run pays before it starts ----------------
|
|
121
|
+
gate("Performance", "fixed per-run context within budget", [
|
|
122
|
+
"bash",
|
|
123
|
+
"pipeline/scripts/smoke-context-budget.sh",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
// --- CI/CD: the workflow audit runs, and finds nothing ---------------------
|
|
127
|
+
// Strict by default, so a runner without the linters fails here rather than
|
|
128
|
+
// reporting a clean audit it never performed.
|
|
129
|
+
gate("CI/CD", "workflow audit clean (zizmor + actionlint)", [
|
|
130
|
+
"bash",
|
|
131
|
+
"pipeline/scripts/smoke-workflow-audit.sh",
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// --- Code quality: our own punctuation rule, both encodings ---------------
|
|
135
|
+
gate("Code quality", "no banned punctuation in shipped files", [
|
|
136
|
+
"bash",
|
|
137
|
+
"pipeline/scripts/smoke-own-punctuation.sh",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// --- Documentation: every skill description routes ------------------------
|
|
141
|
+
gate("Documentation", "every skill description has a routing clause", [
|
|
142
|
+
"node",
|
|
143
|
+
"pipeline/scripts/lint-skills.mjs",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
// --- Documentation: the licence and changelog a consumer expects ----------
|
|
147
|
+
{
|
|
148
|
+
const missing = ["LICENSE", "CHANGELOG.md", "README.md", "SECURITY.md"].filter(
|
|
149
|
+
(f) => !existsSync(join(ROOT, f)),
|
|
150
|
+
);
|
|
151
|
+
measured(
|
|
152
|
+
"Documentation",
|
|
153
|
+
"licence, changelog, readme, security present",
|
|
154
|
+
missing.length === 0,
|
|
155
|
+
missing.length ? `missing: ${missing.join(", ")}` : "all four present",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Tests: no suite reports success without asserting anything ------------
|
|
160
|
+
// The runner owns this check; asking it for the counts would re-run 120 suites,
|
|
161
|
+
// so the scorecard asserts the capability is wired rather than re-measuring it.
|
|
162
|
+
{
|
|
163
|
+
const runner = readFileSync(join(ROOT, "pipeline", "scripts", "run-smokes.mjs"), "utf-8");
|
|
164
|
+
const wired = runner.includes("countAssertions") && /asserted nothing/.test(runner);
|
|
165
|
+
measured(
|
|
166
|
+
"Tests",
|
|
167
|
+
"runner rejects a suite that asserted nothing",
|
|
168
|
+
wired,
|
|
169
|
+
wired
|
|
170
|
+
? "zero-assertion check present in run-smokes.mjs"
|
|
171
|
+
: "run-smokes.mjs no longer counts assertions",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Drift: needs local configuration, so it reports honestly -------------
|
|
176
|
+
{
|
|
177
|
+
// The exit code alone cannot answer this. check-derived-drift exits 0 both
|
|
178
|
+
// when every derivation is current AND when nothing is configured at all, and
|
|
179
|
+
// those are different facts: one is a pass, the other is an absence of
|
|
180
|
+
// evidence. Reading `configured` off the JSON is what tells them apart. The
|
|
181
|
+
// first version of this block trusted exit 0 and reported an unconfigured
|
|
182
|
+
// machine as a passing metric, which is the exact failure this whole gate set
|
|
183
|
+
// exists to prevent.
|
|
184
|
+
let state;
|
|
185
|
+
let ok = false;
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(
|
|
189
|
+
execFileSync("node", ["pipeline/scripts/check-derived-drift.mjs", "--json"], {
|
|
190
|
+
cwd: ROOT,
|
|
191
|
+
encoding: "utf-8",
|
|
192
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// Non-zero exit still writes the report to stdout; an unparseable one means
|
|
197
|
+
// the check itself could not run.
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(e.stdout);
|
|
200
|
+
} catch {
|
|
201
|
+
parsed = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (parsed && parsed.configured > 0) {
|
|
205
|
+
const bad = parsed.drifted + parsed.unverified + parsed.unreachable;
|
|
206
|
+
ok = bad === 0;
|
|
207
|
+
state = ok
|
|
208
|
+
? `${parsed.configured} derivation(s) current with upstream`
|
|
209
|
+
: `${parsed.drifted} drifted, ${parsed.unverified} unverified, ${parsed.unreachable} unreachable`;
|
|
210
|
+
} else if (parsed) {
|
|
211
|
+
state = "NOT CONFIGURED - prefs carry no derivedSkillSources on this machine";
|
|
212
|
+
} else {
|
|
213
|
+
state = "NOT CONFIGURED - the drift check produced no parseable report";
|
|
214
|
+
}
|
|
215
|
+
if (state.startsWith("NOT CONFIGURED")) {
|
|
216
|
+
unmeasured("Derived-skill drift", state);
|
|
217
|
+
} else {
|
|
218
|
+
measured("Architecture", "derived skills current with upstream", ok, state);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- What no script can score --------------------------------------------
|
|
223
|
+
unmeasured("Architecture", "layer separation and dependency direction are judgement, not a number");
|
|
224
|
+
unmeasured("DevEx", "onboarding time and error-message quality need a person");
|
|
225
|
+
unmeasured(
|
|
226
|
+
"Accessibility",
|
|
227
|
+
"nothing in this repo renders UI; the audits it ships are scored per target app",
|
|
228
|
+
);
|
|
229
|
+
unmeasured(
|
|
230
|
+
"Code quality",
|
|
231
|
+
"naming, cohesion and altitude are judgement; only the mechanical rules above are gated",
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const failed = results.filter((r) => r.kind === "MEASURED" && !r.ok);
|
|
235
|
+
const passed = results.filter((r) => r.kind === "MEASURED" && r.ok);
|
|
236
|
+
|
|
237
|
+
if (asJson) {
|
|
238
|
+
console.log(JSON.stringify({ passed: passed.length, failed: failed.length, results }, null, 2));
|
|
239
|
+
} else {
|
|
240
|
+
console.log("MEASURED");
|
|
241
|
+
for (const r of results.filter((x) => x.kind === "MEASURED")) {
|
|
242
|
+
console.log(` ${r.ok ? "✓" : "✗"} ${r.category}: ${r.metric}`);
|
|
243
|
+
console.log(` ${r.detail}`);
|
|
244
|
+
}
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log("UNMEASURED (reported, never scored)");
|
|
247
|
+
for (const r of results.filter((x) => x.kind === "UNMEASURED")) {
|
|
248
|
+
console.log(` - ${r.category}: ${r.why}`);
|
|
249
|
+
}
|
|
250
|
+
console.log("");
|
|
251
|
+
console.log(`══ scorecard: ${passed.length} measured metrics passed, ${failed.length} failed ══`);
|
|
252
|
+
if (failed.length === 0) {
|
|
253
|
+
console.log("Every mechanically checkable claim holds. The four unmeasured");
|
|
254
|
+
console.log("categories above are not covered by that statement.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
process.exit(failed.length === 0 ? 0 : 1);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-context-budget.sh - the bytes every pipeline run pays before it starts.
|
|
4
|
+
#
|
|
5
|
+
# core/multi-agent/SKILL.md plus multi-agent-refs/rules.md load on every run of
|
|
6
|
+
# every mode, whatever the task. They were 67150 bytes together, roughly 17k
|
|
7
|
+
# tokens spent before the first phase, because the orchestrator skill had grown
|
|
8
|
+
# its own inline copies of things that already existed as loadable references -
|
|
9
|
+
# the log-file template (whose copy had gone stale and named a path the code no
|
|
10
|
+
# longer uses), the help text the multi-agent-help skill owns, and a component
|
|
11
|
+
# generation guide that only applies to design tasks.
|
|
12
|
+
#
|
|
13
|
+
# A budget nobody measures only moves one way, so this pins it. The ceiling is
|
|
14
|
+
# the measured value plus a small margin: enough to add a paragraph, not enough
|
|
15
|
+
# to move a chapter back in. Lowering it is always fine.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes: 0=within budget, 1=over, 2=a tracked file is missing
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
22
|
+
|
|
23
|
+
# The files loaded unconditionally at run start.
|
|
24
|
+
TRACKED="
|
|
25
|
+
pipeline/skills/shared/core/multi-agent/SKILL.md
|
|
26
|
+
pipeline/multi-agent-refs/rules.md
|
|
27
|
+
"
|
|
28
|
+
|
|
29
|
+
# Measured 57289 after the split. 60000 leaves ~2.7 kB of headroom.
|
|
30
|
+
CEILING=${CONTEXT_BUDGET_CEILING:-60000}
|
|
31
|
+
|
|
32
|
+
PASS=0
|
|
33
|
+
FAIL=0
|
|
34
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
35
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
36
|
+
|
|
37
|
+
echo "→ 1. every tracked file exists"
|
|
38
|
+
TOTAL=0
|
|
39
|
+
for rel in $TRACKED; do
|
|
40
|
+
f="$REPO_ROOT/$rel"
|
|
41
|
+
if [ ! -f "$f" ]; then
|
|
42
|
+
echo " ✗ missing: $rel"
|
|
43
|
+
echo " a tracked file that vanished makes the budget meaningless"
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
n=$(wc -c < "$f" | tr -d ' ')
|
|
47
|
+
TOTAL=$((TOTAL + n))
|
|
48
|
+
pass "$rel ($n bytes)"
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
echo "→ 2. fixed per-run load is within budget"
|
|
52
|
+
if [ "$TOTAL" -le "$CEILING" ]; then
|
|
53
|
+
pass "fixed load $TOTAL bytes, ceiling $CEILING"
|
|
54
|
+
else
|
|
55
|
+
fail "fixed load $TOTAL bytes exceeds the $CEILING ceiling by $((TOTAL - CEILING))"
|
|
56
|
+
echo " Move detail into multi-agent-refs/ and point at it, rather than"
|
|
57
|
+
echo " raising the ceiling. Every byte here is paid by every run."
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "→ 3. the orchestrator points at the references instead of copying them"
|
|
61
|
+
SKILL="$REPO_ROOT/pipeline/skills/shared/core/multi-agent/SKILL.md"
|
|
62
|
+
REFS=$(grep -c "multi-agent-refs" "$SKILL" || true)
|
|
63
|
+
if [ "$REFS" -ge 3 ]; then
|
|
64
|
+
pass "orchestrator cites multi-agent-refs $REFS times"
|
|
65
|
+
else
|
|
66
|
+
fail "orchestrator cites multi-agent-refs only $REFS time(s)"
|
|
67
|
+
echo " It carried zero citations while duplicating three of those files."
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo ""
|
|
71
|
+
echo "══ smoke-context-budget: $PASS passed, $FAIL failed ══"
|
|
72
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -70,7 +70,7 @@ if [ "$FABLE_COUNT" -ge 4 ]; then
|
|
|
70
70
|
else
|
|
71
71
|
fail "expected >=4 fable personas (architects + Reviewer-1), found $FABLE_COUNT"
|
|
72
72
|
fi
|
|
73
|
-
# Opus stays a valid tier
|
|
73
|
+
# Opus stays a valid tier - the first fallback below fable, and the top tier for security personas.
|
|
74
74
|
OPUS_COUNT=$(grep -l "^preferredModel: opus" "$REPO_ROOT"/pipeline/agents/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
75
75
|
if [ "$OPUS_COUNT" -ge 1 ]; then
|
|
76
76
|
ok "opus personas intact ($OPUS_COUNT files)"
|
|
@@ -7,20 +7,30 @@
|
|
|
7
7
|
# Phase 1; any entry in state.telemetry.mcpCalls[] with phase >= 2 is a
|
|
8
8
|
# violation.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# --state-file <path> Optional override (default: ~/.claude/state/run/latest.json)
|
|
10
|
+
# Two modes.
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
# - State file missing -> WARN, exit 0 (no run to check)
|
|
12
|
+
# CHECK MODE (--state-file <path>): audit that one state file.
|
|
15
13
|
# - telemetry.mcpCalls field absent -> PASS, exit 0 (older run, no telemetry)
|
|
16
14
|
# - All entries phase < 2 -> PASS, exit 0
|
|
17
15
|
# - One or more violations -> FAIL summary on stdout, exit 1
|
|
16
|
+
# - Missing / corrupt file -> exit 2
|
|
17
|
+
#
|
|
18
|
+
# SELF-TEST MODE (no arguments): drive check mode over fixtures that cover
|
|
19
|
+
# every verdict, then audit the live state file if one happens to exist.
|
|
20
|
+
#
|
|
21
|
+
# Why: with no --state-file this script used to print "no run to check" and
|
|
22
|
+
# exit 0. On a clean clone, on CI, and on any machine that had not just
|
|
23
|
+
# finished a pipeline run - which is nearly always - the gate for Locked
|
|
24
|
+
# decision 30 therefore asserted nothing while reporting success. A gate whose
|
|
25
|
+
# green tick means "I had nothing to look at" is indistinguishable from one
|
|
26
|
+
# that passed, and this suite had three of those.
|
|
18
27
|
#
|
|
19
28
|
# stdout: human-readable summary only. stderr: empty in normal operation.
|
|
20
29
|
|
|
21
30
|
set -euo pipefail
|
|
22
31
|
|
|
23
32
|
STATE_FILE="${HOME}/.claude/state/run/latest.json"
|
|
33
|
+
STATE_FILE_EXPLICIT=0
|
|
24
34
|
|
|
25
35
|
while [ "$#" -gt 0 ]; do
|
|
26
36
|
case "$1" in
|
|
@@ -31,16 +41,19 @@ while [ "$#" -gt 0 ]; do
|
|
|
31
41
|
exit 2
|
|
32
42
|
fi
|
|
33
43
|
STATE_FILE="$1"
|
|
44
|
+
STATE_FILE_EXPLICIT=1
|
|
34
45
|
shift
|
|
35
46
|
;;
|
|
36
47
|
--state-file=*)
|
|
37
48
|
STATE_FILE="${1#--state-file=}"
|
|
49
|
+
STATE_FILE_EXPLICIT=1
|
|
38
50
|
shift
|
|
39
51
|
;;
|
|
40
52
|
-h|--help)
|
|
41
53
|
echo "Usage: $0 [--state-file <path>]"
|
|
42
54
|
echo ""
|
|
43
|
-
echo "
|
|
55
|
+
echo "No arguments: run the fixture self-test suite."
|
|
56
|
+
echo "Default state file when checking live: ~/.claude/state/run/latest.json"
|
|
44
57
|
exit 0
|
|
45
58
|
;;
|
|
46
59
|
*)
|
|
@@ -55,9 +68,75 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
55
68
|
exit 2
|
|
56
69
|
fi
|
|
57
70
|
|
|
71
|
+
# ── self-test mode ─────────────────────────────────────────────────────────
|
|
72
|
+
if [ "$STATE_FILE_EXPLICIT" -eq 0 ]; then
|
|
73
|
+
SELF="$0"
|
|
74
|
+
TMP=$(mktemp -d)
|
|
75
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
76
|
+
PASS=0
|
|
77
|
+
FAIL=0
|
|
78
|
+
|
|
79
|
+
expect() {
|
|
80
|
+
local name="$1" want="$2" file="$3"
|
|
81
|
+
local got=0
|
|
82
|
+
bash "$SELF" --state-file "$file" >/dev/null 2>&1 || got=$?
|
|
83
|
+
if [ "$got" -eq "$want" ]; then
|
|
84
|
+
PASS=$((PASS + 1)); echo " ✓ $name (exit $got)"
|
|
85
|
+
else
|
|
86
|
+
FAIL=$((FAIL + 1)); echo " ✗ $name (exit $got, expected $want)"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
echo "→ Locked decision 30 gate, fixture self-test"
|
|
91
|
+
|
|
92
|
+
printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"mcp__figma__get_design_context","phase":1}]}}' \
|
|
93
|
+
> "$TMP/phase1-only.json"
|
|
94
|
+
expect "MCP call in Phase 1 passes" 0 "$TMP/phase1-only.json"
|
|
95
|
+
|
|
96
|
+
printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"mcp__figma__get_design_context","phase":3}]}}' \
|
|
97
|
+
> "$TMP/phase3.json"
|
|
98
|
+
expect "MCP call in Phase 3 is a violation" 1 "$TMP/phase3.json"
|
|
99
|
+
|
|
100
|
+
printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"a","phase":1},{"tool":"b","phase":2}]}}' \
|
|
101
|
+
> "$TMP/mixed.json"
|
|
102
|
+
expect "one violation among clean entries still fails" 1 "$TMP/mixed.json"
|
|
103
|
+
|
|
104
|
+
printf '%s' '{"telemetry":{"mcpCalls":[]}}' > "$TMP/empty.json"
|
|
105
|
+
expect "empty call list passes" 0 "$TMP/empty.json"
|
|
106
|
+
|
|
107
|
+
printf '%s' '{"telemetry":{}}' > "$TMP/no-field.json"
|
|
108
|
+
expect "absent telemetry.mcpCalls passes (older state format)" 0 "$TMP/no-field.json"
|
|
109
|
+
|
|
110
|
+
printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"a"}]}}' > "$TMP/no-phase.json"
|
|
111
|
+
expect "entry with no phase defaults to Phase 0 and passes" 0 "$TMP/no-phase.json"
|
|
112
|
+
|
|
113
|
+
printf '%s' 'not json at all' > "$TMP/corrupt.json"
|
|
114
|
+
expect "corrupt state is an error, never a silent pass" 2 "$TMP/corrupt.json"
|
|
115
|
+
|
|
116
|
+
expect "missing state file is an error in check mode" 2 "$TMP/does-not-exist.json"
|
|
117
|
+
|
|
118
|
+
# The live run, when there is one. Absence is not a failure here - the
|
|
119
|
+
# fixtures above already proved the gate works.
|
|
120
|
+
if [ -f "$STATE_FILE" ]; then
|
|
121
|
+
echo "→ live state file"
|
|
122
|
+
if bash "$SELF" --state-file "$STATE_FILE"; then
|
|
123
|
+
PASS=$((PASS + 1)); echo " ✓ live run clean"
|
|
124
|
+
else
|
|
125
|
+
FAIL=$((FAIL + 1)); echo " ✗ live run has violations"
|
|
126
|
+
fi
|
|
127
|
+
else
|
|
128
|
+
echo "→ live state file: none at $STATE_FILE (fixtures already exercised every path)"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
echo ""
|
|
132
|
+
echo "══ smoke-no-mcp-in-dev-phases: $PASS passed, $FAIL failed ══"
|
|
133
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# ── check mode ─────────────────────────────────────────────────────────────
|
|
58
137
|
if [ ! -f "$STATE_FILE" ]; then
|
|
59
|
-
echo "
|
|
60
|
-
exit
|
|
138
|
+
echo "FAIL: state file not found at $STATE_FILE"
|
|
139
|
+
exit 2
|
|
61
140
|
fi
|
|
62
141
|
|
|
63
142
|
# Validate JSON parse first; corrupt state cannot be silently passed.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-own-punctuation.sh - the punctuation rule applies to this repo's own
|
|
4
|
+
# shipped files, not only to the documents it generates.
|
|
5
|
+
#
|
|
6
|
+
# validate-analysis-doc.mjs has enforced the locked policy (no em-dash, en-dash,
|
|
7
|
+
# ellipsis or section sign) on generated analysis documents for a long time,
|
|
8
|
+
# and smoke-generate-issue.sh / smoke-review-readiness.sh enforce it on issue
|
|
9
|
+
# and review payloads. Nothing applied it to the pipeline's own tree, so 77
|
|
10
|
+
# shipped files carried 719 em-dashes, 10 en-dashes and 24 ellipses while the
|
|
11
|
+
# rule was described as enforced.
|
|
12
|
+
#
|
|
13
|
+
# ALLOWED holds the files that carry these characters as data rather than
|
|
14
|
+
# prose: the detector's own table and the grep patterns that look for them.
|
|
15
|
+
# Stripping those would disable the very checks this rule depends on. Every
|
|
16
|
+
# entry needs a reason, and the list is expected to stay this short.
|
|
17
|
+
#
|
|
18
|
+
# Exit codes: 0=clean, 1=violations
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
23
|
+
cd "$REPO_ROOT"
|
|
24
|
+
|
|
25
|
+
# file path -> why the characters are legitimately present
|
|
26
|
+
ALLOWED_PATHS="
|
|
27
|
+
pipeline/scripts/validate-analysis-doc.mjs|the detector's own table of banned characters
|
|
28
|
+
pipeline/scripts/smoke-generate-issue.sh|greps for the banned characters
|
|
29
|
+
pipeline/scripts/smoke-review-readiness.sh|greps for the banned characters
|
|
30
|
+
pipeline/scripts/smoke-md2confluence.sh|dirty-input fixture plus the forbidden list
|
|
31
|
+
pipeline/scripts/smoke-own-punctuation.sh|this file names the characters it bans
|
|
32
|
+
pipeline/lib/md2confluence-v3.py|the converter's codepoint-to-ASCII mapping table
|
|
33
|
+
"
|
|
34
|
+
|
|
35
|
+
# The same characters written as escapes. prefs.schema.json carried 50 of them
|
|
36
|
+
# in its descriptions and was invisible to a search for the literal character,
|
|
37
|
+
# so the first version of this gate passed it. A rule that only sees one
|
|
38
|
+
# encoding of what it bans is not enforcing the rule.
|
|
39
|
+
ESCAPED='\\u2014|\\u2013|\\u2026|\\u00a7'
|
|
40
|
+
|
|
41
|
+
is_allowed() {
|
|
42
|
+
printf '%s\n' "$ALLOWED_PATHS" | grep -q "^$1|"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
PASS=0
|
|
46
|
+
FAIL=0
|
|
47
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
48
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
49
|
+
|
|
50
|
+
echo "→ 1. no em-dash, en-dash, ellipsis or section sign in shipped files"
|
|
51
|
+
|
|
52
|
+
# CHANGELOG history is left as written; rewriting released notes to satisfy a
|
|
53
|
+
# later style rule would falsify the record.
|
|
54
|
+
VIOLATIONS=""
|
|
55
|
+
while IFS= read -r f; do
|
|
56
|
+
case "$f" in CHANGELOG*.md) continue ;; esac
|
|
57
|
+
is_allowed "$f" && continue
|
|
58
|
+
VIOLATIONS="${VIOLATIONS}${f}\n"
|
|
59
|
+
done < <(git grep -lI -e $'—' -e $'–' -e $'…' -e $'§' -- . 2>/dev/null || true)
|
|
60
|
+
|
|
61
|
+
if [ -z "$VIOLATIONS" ]; then
|
|
62
|
+
pass "no banned punctuation outside the allowlist"
|
|
63
|
+
else
|
|
64
|
+
COUNT=$(printf '%b' "$VIOLATIONS" | grep -c . || true)
|
|
65
|
+
fail "$COUNT file(s) carry banned punctuation"
|
|
66
|
+
printf '%b' "$VIOLATIONS" | sed 's/^/ /'
|
|
67
|
+
echo " fix: plain ASCII - or : or parentheses; 'section 4' rather than the sign"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "→ 2. same characters written as unicode escapes"
|
|
71
|
+
ESC_VIOLATIONS=""
|
|
72
|
+
while IFS= read -r f; do
|
|
73
|
+
case "$f" in CHANGELOG*.md) continue ;; esac
|
|
74
|
+
is_allowed "$f" && continue
|
|
75
|
+
ESC_VIOLATIONS="${ESC_VIOLATIONS}${f}\n"
|
|
76
|
+
done < <(git grep -lI -E "$ESCAPED" -- . 2>/dev/null || true)
|
|
77
|
+
|
|
78
|
+
if [ -z "$ESC_VIOLATIONS" ]; then
|
|
79
|
+
pass "no escaped forms outside the allowlist"
|
|
80
|
+
else
|
|
81
|
+
COUNT=$(printf '%b' "$ESC_VIOLATIONS" | grep -c . || true)
|
|
82
|
+
fail "$COUNT file(s) carry the banned characters as unicode escapes"
|
|
83
|
+
printf '%b' "$ESC_VIOLATIONS" | sed 's/^/ /'
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo "→ 3. every allowlist entry still exists and still needs the exemption"
|
|
87
|
+
while IFS='|' read -r path reason; do
|
|
88
|
+
[ -z "$path" ] && continue
|
|
89
|
+
if [ ! -f "$path" ]; then
|
|
90
|
+
fail "allowlisted path no longer exists: $path"
|
|
91
|
+
elif ! grep -qI -e $'—' -e $'–' -e $'…' -e $'§' "$path" 2>/dev/null \
|
|
92
|
+
&& ! grep -qIE "$ESCAPED" "$path" 2>/dev/null; then
|
|
93
|
+
# Either encoding justifies the entry: md2confluence-v3.py holds them as
|
|
94
|
+
# escapes, the detector table holds them literally.
|
|
95
|
+
fail "$path is allowlisted ($reason) but is now clean - drop it from the list"
|
|
96
|
+
else
|
|
97
|
+
pass "allowlist justified: ${path##*/} ($reason)"
|
|
98
|
+
fi
|
|
99
|
+
done < <(printf '%s\n' "$ALLOWED_PATHS" | grep '|')
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "══ smoke-own-punctuation: $PASS passed, $FAIL failed ══"
|
|
103
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -4,20 +4,38 @@
|
|
|
4
4
|
# `.github/workflows/` with the two dedicated external linters:
|
|
5
5
|
#
|
|
6
6
|
# - zizmor GitHub Actions security auditor (template injection,
|
|
7
|
-
# credential persistence, unpinned actions,
|
|
8
|
-
#
|
|
7
|
+
# credential persistence, cache poisoning, unpinned actions,
|
|
8
|
+
# excessive permissions, ...). Runs offline, no network.
|
|
9
9
|
# - actionlint workflow correctness linter (expression syntax, matrix
|
|
10
10
|
# typos, shellcheck of run: blocks, ...).
|
|
11
11
|
#
|
|
12
|
-
# Both tools are optional: each is skipped with a SKIP line when its binary
|
|
13
|
-
# is not on PATH (billing-friendly CI runners install neither; run locally
|
|
14
|
-
# via `brew install zizmor actionlint` for full coverage). Structural
|
|
15
|
-
# baseline checks that need no external binary live in
|
|
16
|
-
# smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
|
|
17
|
-
#
|
|
18
12
|
# Any finding from either tool fails the smoke.
|
|
19
13
|
#
|
|
20
|
-
#
|
|
14
|
+
# WHY THIS IS STRICT BY DEFAULT
|
|
15
|
+
#
|
|
16
|
+
# Until now a missing binary produced a SKIP and the smoke exited 0, so it
|
|
17
|
+
# reported "0 passed, 0 failed, 2 skipped" on the dev machine AND on the CI
|
|
18
|
+
# runner - neither had the tools. ci-lite.yml justified not installing them by
|
|
19
|
+
# saying to run the smoke locally "where the tools exist", but they did not
|
|
20
|
+
# exist there either. The audit had therefore never run anywhere, while
|
|
21
|
+
# appearing as a green step. When it was finally run it found nine real issues,
|
|
22
|
+
# one of them high severity (a dependency cache restored into the job that
|
|
23
|
+
# signs and publishes the release tarball).
|
|
24
|
+
#
|
|
25
|
+
# So the failure mode is inverted: absent tooling is a failure, and skipping is
|
|
26
|
+
# something you opt into deliberately and locally.
|
|
27
|
+
#
|
|
28
|
+
# brew install zizmor actionlint # macOS
|
|
29
|
+
# pipx install zizmor && go install github.com/rhysd/actionlint/cmd/actionlint@latest
|
|
30
|
+
#
|
|
31
|
+
# WORKFLOW_AUDIT_ALLOW_MISSING=1 downgrades a missing binary back to a SKIP for
|
|
32
|
+
# a local run. It is ignored when CI is set: CI installs both, pinned, and a
|
|
33
|
+
# runner that cannot must fail loudly rather than pass quietly.
|
|
34
|
+
#
|
|
35
|
+
# Structural baseline checks that need no external binary live in
|
|
36
|
+
# smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
|
|
37
|
+
#
|
|
38
|
+
# Exit codes: 0=pass, 1=findings or missing tooling, 2=workflows missing
|
|
21
39
|
|
|
22
40
|
set -uo pipefail
|
|
23
41
|
|
|
@@ -26,6 +44,10 @@ WF_DIR="$REPO_ROOT/.github/workflows"
|
|
|
26
44
|
|
|
27
45
|
[ -d "$WF_DIR" ] || { echo "FAIL: .github/workflows/ missing"; exit 2; }
|
|
28
46
|
|
|
47
|
+
# In CI the escape hatch does not apply, whatever the environment says.
|
|
48
|
+
ALLOW_MISSING="${WORKFLOW_AUDIT_ALLOW_MISSING:-0}"
|
|
49
|
+
[ -n "${CI:-}" ] && ALLOW_MISSING=0
|
|
50
|
+
|
|
29
51
|
PASS=0
|
|
30
52
|
FAIL=0
|
|
31
53
|
SKIPPED=0
|
|
@@ -33,6 +55,16 @@ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
|
33
55
|
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
34
56
|
skip() { SKIPPED=$((SKIPPED + 1)); echo " ↷ SKIP: $1"; }
|
|
35
57
|
|
|
58
|
+
# A tool that is not installed is a failure unless a local run opted out.
|
|
59
|
+
missing_tool() {
|
|
60
|
+
local tool="$1" install="$2"
|
|
61
|
+
if [ "$ALLOW_MISSING" = "1" ]; then
|
|
62
|
+
skip "$tool not on PATH, WORKFLOW_AUDIT_ALLOW_MISSING=1 ($install)"
|
|
63
|
+
else
|
|
64
|
+
fail "$tool not on PATH - the audit cannot run ($install)"
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
# ──────────────────────────────────────────────────────────────────────────
|
|
37
69
|
echo "→ 1. zizmor (security audit, offline)"
|
|
38
70
|
if command -v zizmor >/dev/null 2>&1; then
|
|
@@ -48,7 +80,7 @@ if command -v zizmor >/dev/null 2>&1; then
|
|
|
48
80
|
echo "$OUT" | sed 's/^/ /'
|
|
49
81
|
fi
|
|
50
82
|
else
|
|
51
|
-
|
|
83
|
+
missing_tool zizmor "brew install zizmor"
|
|
52
84
|
fi
|
|
53
85
|
|
|
54
86
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@@ -61,7 +93,7 @@ if command -v actionlint >/dev/null 2>&1; then
|
|
|
61
93
|
echo "$OUT" | sed 's/^/ /'
|
|
62
94
|
fi
|
|
63
95
|
else
|
|
64
|
-
|
|
96
|
+
missing_tool actionlint "brew install actionlint"
|
|
65
97
|
fi
|
|
66
98
|
|
|
67
99
|
echo ""
|