@event4u/agent-config 8.13.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +183 -0
- package/MIGRATION.md +14 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/agent-handoff.md +35 -0
- package/dist/agent-src/commands/agents/init.md +1 -0
- package/dist/agent-src/commands/agents/optimize.md +1 -0
- package/dist/agent-src/commands/agents/user/accept.md +3 -2
- package/dist/agent-src/commands/agents/user/init.md +5 -4
- package/dist/agent-src/commands/agents/user/update.md +3 -2
- package/dist/agent-src/commands/agents/user.md +1 -0
- package/dist/agent-src/commands/agents.md +1 -0
- package/dist/agent-src/commands/analytics/show.md +1 -0
- package/dist/agent-src/commands/analytics.md +1 -0
- package/dist/agent-src/commands/analyze/decision.md +1 -0
- package/dist/agent-src/commands/analyze/incident.md +1 -0
- package/dist/agent-src/commands/analyze/near-miss.md +1 -0
- package/dist/agent-src/commands/analyze/postmortem.md +1 -0
- package/dist/agent-src/commands/analyze/premortem.md +1 -0
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -0
- package/dist/agent-src/commands/analyze.md +3 -2
- package/dist/agent-src/commands/brand/identity.md +6 -5
- package/dist/agent-src/commands/brand/review.md +6 -5
- package/dist/agent-src/commands/brand/strategy.md +6 -4
- package/dist/agent-src/commands/brand/tokens.md +7 -6
- package/dist/agent-src/commands/brand/voice.md +5 -4
- package/dist/agent-src/commands/brand.md +9 -7
- package/dist/agent-src/commands/bug/fix.md +1 -0
- package/dist/agent-src/commands/bug/investigate.md +1 -0
- package/dist/agent-src/commands/bug.md +2 -1
- package/dist/agent-src/commands/challenge-me/vision.md +1 -0
- package/dist/agent-src/commands/challenge-me/with-docs.md +1 -0
- package/dist/agent-src/commands/challenge-me.md +28 -6
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/check-current-md.md +1 -0
- package/dist/agent-src/commands/commit.md +1 -0
- package/dist/agent-src/commands/context/create.md +3 -2
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/cost/profile.md +1 -0
- package/dist/agent-src/commands/cost.md +2 -1
- package/dist/agent-src/commands/council/analysis.md +1 -0
- package/dist/agent-src/commands/council/debate.md +1 -0
- package/dist/agent-src/commands/council/default.md +21 -20
- package/dist/agent-src/commands/council/design.md +1 -0
- package/dist/agent-src/commands/council/optimize.md +1 -0
- package/dist/agent-src/commands/council/pr.md +1 -0
- package/dist/agent-src/commands/council.md +1 -0
- package/dist/agent-src/commands/estimate-ticket.md +1 -0
- package/dist/agent-src/commands/explain-run.md +88 -0
- package/dist/agent-src/commands/feature/dev.md +1 -0
- package/dist/agent-src/commands/feature/explore.md +1 -0
- package/dist/agent-src/commands/feature/plan.md +1 -0
- package/dist/agent-src/commands/feature/refactor.md +1 -0
- package/dist/agent-src/commands/feature/roadmap.md +1 -0
- package/dist/agent-src/commands/feature.md +2 -1
- package/dist/agent-src/commands/fix/pr-comments.md +19 -0
- package/dist/agent-src/commands/fix.md +2 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
- package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
- package/dist/agent-src/commands/ghostwriter/show.md +1 -0
- package/dist/agent-src/commands/ghostwriter/write.md +1 -0
- package/dist/agent-src/commands/ghostwriter.md +1 -0
- package/dist/agent-src/commands/grill-me.md +17 -12
- package/dist/agent-src/commands/humanize.md +1 -0
- package/dist/agent-src/commands/image/analyse.md +2 -1
- package/dist/agent-src/commands/image/create.md +1 -0
- package/dist/agent-src/commands/image/verify.md +1 -0
- package/dist/agent-src/commands/image.md +16 -12
- package/dist/agent-src/commands/implement-ticket.md +1 -0
- package/dist/agent-src/commands/judge/on-diff.md +1 -0
- package/dist/agent-src/commands/judge/solo.md +1 -0
- package/dist/agent-src/commands/judge/steps.md +1 -0
- package/dist/agent-src/commands/judge.md +1 -0
- package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
- package/dist/agent-src/commands/knowledge/forget.md +1 -0
- package/dist/agent-src/commands/knowledge/ingest.md +1 -0
- package/dist/agent-src/commands/knowledge/list.md +10 -0
- package/dist/agent-src/commands/knowledge.md +1 -0
- package/dist/agent-src/commands/memory/learn-low-impact.md +1 -0
- package/dist/agent-src/commands/memory/mine-session.md +1 -0
- package/dist/agent-src/commands/memory/promote.md +3 -2
- package/dist/agent-src/commands/memory.md +1 -0
- package/dist/agent-src/commands/mission/upgrade.md +45 -24
- package/dist/agent-src/commands/mode.md +1 -0
- package/dist/agent-src/commands/module/create.md +1 -0
- package/dist/agent-src/commands/module/explore.md +1 -0
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
- package/dist/agent-src/commands/optimize/project.md +1 -0
- package/dist/agent-src/commands/optimize/prompt.md +1 -0
- package/dist/agent-src/commands/optimize.md +2 -1
- package/dist/agent-src/commands/orchestrate.md +1 -0
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/package.md +2 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
- package/dist/agent-src/commands/post-as/me.md +1 -0
- package/dist/agent-src/commands/post-as.md +1 -0
- package/dist/agent-src/commands/pr/create/description-only.md +139 -8
- package/dist/agent-src/commands/pr/create.md +35 -22
- package/dist/agent-src/commands/prediction-pool.md +7 -6
- package/dist/agent-src/commands/prepare-for-review.md +1 -0
- package/dist/agent-src/commands/profile/activate.md +1 -0
- package/dist/agent-src/commands/profile/deactivate.md +1 -0
- package/dist/agent-src/commands/profile.md +1 -0
- package/dist/agent-src/commands/project.md +2 -1
- package/dist/agent-src/commands/refine-ticket.md +7 -0
- package/dist/agent-src/commands/research/deep.md +1 -0
- package/dist/agent-src/commands/research.md +1 -0
- package/dist/agent-src/commands/review/routing.md +1 -0
- package/dist/agent-src/commands/review.md +2 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
- package/dist/agent-src/commands/roadmap/materialize.md +1 -0
- package/dist/agent-src/commands/roadmap/process-full.md +1 -0
- package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
- package/dist/agent-src/commands/roadmap/process-step.md +1 -0
- package/dist/agent-src/commands/roadmap.md +1 -0
- package/dist/agent-src/commands/security-audit-config.md +2 -1
- package/dist/agent-src/commands/skill/preview.md +1 -0
- package/dist/agent-src/commands/skill.md +1 -0
- package/dist/agent-src/commands/skills/discover.md +1 -0
- package/dist/agent-src/commands/skills.md +1 -0
- package/dist/agent-src/commands/sync/agent-settings.md +1 -0
- package/dist/agent-src/commands/sync.md +2 -1
- package/dist/agent-src/commands/team/adversarial.md +3 -2
- package/dist/agent-src/commands/team/delegate.md +3 -2
- package/dist/agent-src/commands/team/review.md +3 -2
- package/dist/agent-src/commands/team/status.md +49 -18
- package/dist/agent-src/commands/team-knowledge.md +8 -6
- package/dist/agent-src/commands/team.md +21 -16
- package/dist/agent-src/commands/tests/e2e-heal.md +1 -0
- package/dist/agent-src/commands/tests/e2e-plan.md +1 -0
- package/dist/agent-src/commands/tests/execute.md +1 -0
- package/dist/agent-src/commands/tests.md +2 -1
- package/dist/agent-src/commands/update-form-request-messages.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +1 -0
- package/dist/agent-src/commands/video/from-script.md +3 -2
- package/dist/agent-src/commands/video/from-song.md +22 -21
- package/dist/agent-src/commands/video/scene.md +1 -0
- package/dist/agent-src/commands/video/stitch.md +2 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -0
- package/dist/agent-src/commands/video.md +1 -0
- package/dist/agent-src/commands/work.md +1 -0
- package/dist/agent-src/commands/worktree/cleanup.md +8 -7
- package/dist/agent-src/commands/worktree/create.md +1 -0
- package/dist/agent-src/commands/worktree/verify.md +1 -0
- package/dist/agent-src/commands/worktree.md +1 -0
- package/dist/agent-src/rules/active-remediation.md +12 -36
- package/dist/agent-src/rules/artifact-drafting-protocol.md +7 -64
- package/dist/agent-src/rules/context-hygiene.md +19 -113
- package/dist/agent-src/rules/design-fidelity.md +4 -60
- package/dist/agent-src/rules/domain-adoption-policy.md +10 -106
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +12 -60
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +6 -86
- package/dist/agent-src/rules/size-enforcement.md +10 -0
- package/dist/agent-src/rules/tool-safety.md +17 -3
- package/dist/agent-src/skills/agent-security-review/SKILL.md +12 -1
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -0
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +24 -2
- package/dist/agent-src/skills/code-review/SKILL.md +105 -93
- package/dist/agent-src/skills/code-review/checklists/backend.md +50 -0
- package/dist/agent-src/skills/code-review/checklists/database.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/dependency.md +18 -0
- package/dist/agent-src/skills/code-review/checklists/docs.md +14 -0
- package/dist/agent-src/skills/code-review/checklists/frontend.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/infra.md +17 -0
- package/dist/agent-src/skills/code-review/checklists/producing-the-review.md +62 -0
- package/dist/agent-src/skills/code-review/checklists/test-only.md +13 -0
- package/dist/agent-src/skills/code-review/evals/evals.json +53 -15
- package/dist/agent-src/skills/condense-memory/SKILL.md +3 -2
- package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +15 -0
- package/dist/agent-src/skills/docker/SKILL.md +10 -0
- package/dist/agent-src/skills/docx-authoring/SKILL.md +102 -0
- package/dist/agent-src/skills/github-ci/evals/evals.json +25 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +79 -3
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +112 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +70 -0
- package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +7 -1
- package/dist/agent-src/skills/laravel/SKILL.md +10 -0
- package/dist/agent-src/skills/launch-readiness/SKILL.md +35 -0
- package/dist/agent-src/skills/llm-provider-knowledge/SKILL.md +146 -0
- package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +20 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +30 -0
- package/dist/agent-src/skills/pdf-tools/SKILL.md +101 -0
- package/dist/agent-src/skills/quality-tools/evals/evals.json +15 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +29 -0
- package/dist/agent-src/skills/security-maturity-assessment/SKILL.md +90 -0
- package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +52 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +79 -0
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +11 -3
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +11 -0
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +103 -0
- package/dist/agent-src/skills/terraform/SKILL.md +10 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +31 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +15 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +10 -4
- package/dist/agent-src/templates/agent-settings.md +28 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/dist/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/dist/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +314 -194
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +23 -18
- package/dist/discovery/trust-report.md +8 -8
- package/dist/discovery/workspaces.json +18 -13
- package/dist/install/install.mjs +48 -33
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +4 -1
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +5 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +10 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +9 -2
- package/docs/MIGRATION.md +20 -0
- package/docs/SKILL_CENSUS.md +8 -0
- package/docs/architecture.md +3 -3
- package/docs/benchmark.md +67 -2
- package/docs/catalog.md +2 -2
- package/docs/command-flows.md +1 -1
- package/docs/contracts/agents-layout.md +1 -1
- package/docs/contracts/ai-council-config.md +9 -2
- package/docs/contracts/ai-team-config.md +37 -4
- package/docs/contracts/consumer-bridge.md +55 -63
- package/docs/contracts/install-layout.md +1 -1
- package/docs/contracts/migrate-command.md +1 -1
- package/docs/contracts/prelaunch-diagnostics.md +151 -0
- package/docs/contracts/release-pr-gating.md +1 -0
- package/docs/customization.md +22 -1
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +34 -0
- package/docs/decisions/ADR-049-configuration-trust-boundary.md +9 -1
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +17 -6
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +45 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +28 -0
- package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +76 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +90 -0
- package/docs/guidelines/agent-infra/domain-adoption-gates.md +112 -0
- package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +99 -0
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +13 -0
- package/docs/guidelines/design-fidelity-mechanics.md +76 -0
- package/docs/installation.md +2 -1
- package/docs/maintainers/dev-mode.md +3 -2
- package/docs/parity/command-frontmatter.md +60 -0
- package/docs/proof.md +7 -6
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/src/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/src/config/agent-settings.template.yml +73 -10
- package/src/config/gitignore-block.txt +16 -6
- package/src/config/prelaunch-areas.yml +36 -0
- package/src/scripts/_cli/cmd_conformance.ts +2 -2
- package/src/scripts/_cli/cmd_doctor.ts +280 -39
- package/src/scripts/_cli/cmd_refresh.ts +35 -118
- package/src/scripts/_lib/agent_settings.ts +4 -1
- package/src/scripts/_lib/claude_plugin.ts +77 -2
- package/src/scripts/_lib/orchestration_record.ts +5 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +120 -0
- package/src/scripts/ai_council/clients.ts +68 -6
- package/src/scripts/ai_council/pricing.ts +62 -0
- package/src/scripts/ai_team/config.ts +73 -1
- package/src/scripts/ai_team/review_gate.ts +636 -0
- package/src/scripts/ai_team/team_dispatch.ts +612 -0
- package/src/scripts/bench_ab_task_runner.ts +11 -1
- package/src/scripts/bench_parity_count.ts +279 -0
- package/src/scripts/build_linear_digest.ts +3 -1
- package/src/scripts/check_bridge_derivation.ts +172 -0
- package/src/scripts/check_consumer_scope_flip.ts +179 -0
- package/src/scripts/check_council_config_location.ts +115 -15
- package/src/scripts/check_host_loadability.ts +81 -0
- package/src/scripts/condense.ts +5 -0
- package/src/scripts/council_cli.ts +19 -3
- package/src/scripts/hook_manifest.yaml +12 -1
- package/src/scripts/install.ts +44 -45
- package/src/scripts/lint_agents_layout.ts +10 -4
- package/src/scripts/lint_breaking_changes_index.ts +303 -0
- package/src/scripts/lint_handoffs.ts +42 -0
- package/src/scripts/lint_skill_descriptions.ts +228 -0
- package/src/scripts/lint_skill_descriptions_allowlist.json +1 -0
- package/src/scripts/lint_skill_scripts_readonly.ts +145 -0
- package/src/scripts/lint_skill_scripts_readonly_allowlist.json +8 -0
- package/src/scripts/lint_supported_tools_matrix.ts +35 -0
- package/src/scripts/mcp_server/tools.ts +1 -3
- package/src/scripts/optimize_skill_description.ts +144 -0
- package/src/scripts/orchestration_record.ts +2 -1
- package/src/scripts/prelaunch_diagnostics.ts +248 -0
- package/src/scripts/run_skill_evals.ts +70 -4
- package/src/scripts/schemas/agent-settings.schema.json +17 -0
- package/src/scripts/schemas/command.schema.json +7 -0
- package/src/scripts/schemas/evals.schema.json +105 -14
- package/src/scripts/schemas/prelaunch-report.schema.json +107 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/schemas/subagent.schema.json +6 -0
- package/src/scripts/self_review_gate.test.ts +11 -0
- package/src/scripts/self_review_gate.ts +7 -1
- package/src/scripts/skill_eval_coverage.ts +4 -1
- package/src/scripts/skill_linter.ts +43 -3
- package/src/scripts/smoke_host_loadability.sh +73 -0
- package/src/scripts/team_review_gate_hook.ts +115 -0
- package/src/scripts/trigger_eval_grandfather.json +1 -2
- package/src/scripts/utilization_report.ts +225 -0
- package/src/server/schemas/settings.ts +24 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* BREAKING_CHANGES.md ↔ CHANGELOG sync linter (road-to-feedback-9.0 P1.4).
|
|
4
|
+
*
|
|
5
|
+
* `BREAKING_CHANGES.md` is the discoverable index of consumer-facing breaking
|
|
6
|
+
* changes, one per major (`X.0.0`). When a diff ships a NEW breaking entry in a
|
|
7
|
+
* RELEASED CHANGELOG section it must also touch the index in the same change,
|
|
8
|
+
* or the index silently rots behind the changelog.
|
|
9
|
+
*
|
|
10
|
+
* TRIGGER: a git diff (working tree vs `origin/main` — staged + unstaged) that
|
|
11
|
+
* ADDS a `### BREAKING` / `### BREAKING CHANGES` subheading under a RELEASED
|
|
12
|
+
* major section (`## [N.0.0]`) in `CHANGELOG.md`, where the same major had no
|
|
13
|
+
* such subheading at the base ref (a wholly-new released section counts). A
|
|
14
|
+
* breaking note under `## [Unreleased]` is WIP and never trips the gate.
|
|
15
|
+
*
|
|
16
|
+
* REQUIREMENT: when triggered, the same diff must also modify
|
|
17
|
+
* `BREAKING_CHANGES.md`.
|
|
18
|
+
*
|
|
19
|
+
* Escape hatch (mirrors `check_structural_breaking.ts`'s `ci-override` trailer):
|
|
20
|
+
* an own-line trailer `ci-override: breaking-index-override` (the bare token
|
|
21
|
+
* `breaking-index-override` on its own line is also accepted) in any commit
|
|
22
|
+
* body in the range, or the env var `BREAKING_INDEX_OVERRIDE=1`, clears the
|
|
23
|
+
* gate for an intentional index-deferred change.
|
|
24
|
+
*
|
|
25
|
+
* CLI: exit 0 clean/cleared, 1 violation (or selftest failure), 2 usage error.
|
|
26
|
+
* `--selftest` proves the rule on inline string fixtures (no git needed).
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
34
|
+
// src/scripts/lint_breaking_changes_index.ts → two levels up is the repo root.
|
|
35
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
36
|
+
const BASE_REF = 'origin/main';
|
|
37
|
+
const CHANGELOG_REL = 'CHANGELOG.md';
|
|
38
|
+
const INDEX_REL = 'BREAKING_CHANGES.md';
|
|
39
|
+
|
|
40
|
+
// A RELEASED major section heading: `## [N.0.0]…` (minor + patch both 0).
|
|
41
|
+
// `## [Unreleased]` never matches, so WIP breaking notes are out of scope.
|
|
42
|
+
const RELEASED_MAJOR_HEADING_RE = /^## \[(?<version>\d+\.0\.0)\]/;
|
|
43
|
+
// Any other level-1/2 heading closes the current section.
|
|
44
|
+
const SECTION_BOUNDARY_RE = /^##? /;
|
|
45
|
+
// A breaking subheading as emitted by release-please: `### BREAKING` or
|
|
46
|
+
// `### BREAKING CHANGES` (all-caps). Mixed-case `### Breaking changes` under
|
|
47
|
+
// Unreleased is intentionally NOT matched.
|
|
48
|
+
const BREAKING_SUBHEADING_RE = /^### BREAKING\b/;
|
|
49
|
+
// Override trailer — same shape as check_structural_breaking's `ci-override:`
|
|
50
|
+
// mechanism (own line, not an inline mention); the bare token is also accepted.
|
|
51
|
+
const OVERRIDE_RE = /^(?:ci-override:\s*)?breaking-index-override\s*$/m;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return the set of RELEASED major versions whose CHANGELOG section contains a
|
|
55
|
+
* `### BREAKING` subheading.
|
|
56
|
+
*/
|
|
57
|
+
export function released_breaking_majors(lines: readonly string[]): Set<string> {
|
|
58
|
+
const found = new Set<string>();
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const m = RELEASED_MAJOR_HEADING_RE.exec(lines[i]!);
|
|
61
|
+
if (!m) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const version = m.groups!['version']!;
|
|
65
|
+
// Section body: heading line down to (exclusive) the next boundary.
|
|
66
|
+
let end = lines.length;
|
|
67
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
68
|
+
if (SECTION_BOUNDARY_RE.test(lines[j]!)) {
|
|
69
|
+
end = j;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (lines.slice(i + 1, end).some((ln) => BREAKING_SUBHEADING_RE.test(ln))) {
|
|
74
|
+
found.add(version);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Core check. Returns the sorted list of RELEASED major versions that GAINED a
|
|
82
|
+
* `### BREAKING` subheading between `oldLines` and `newLines` — the versions
|
|
83
|
+
* that should be reflected in `BREAKING_CHANGES.md`.
|
|
84
|
+
*/
|
|
85
|
+
export function added_released_breaking_majors(
|
|
86
|
+
oldLines: readonly string[],
|
|
87
|
+
newLines: readonly string[],
|
|
88
|
+
): string[] {
|
|
89
|
+
const before = released_breaking_majors(oldLines);
|
|
90
|
+
const after = released_breaking_majors(newLines);
|
|
91
|
+
const added = [...after].filter((v) => !before.has(v));
|
|
92
|
+
added.sort();
|
|
93
|
+
return added;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CheckResult {
|
|
97
|
+
readonly addedMajors: readonly string[];
|
|
98
|
+
readonly indexChanged: boolean;
|
|
99
|
+
readonly overridden: boolean;
|
|
100
|
+
readonly violation: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Pure decision: violation iff a released BREAKING was added, the index was not touched, and no override cleared it. */
|
|
104
|
+
export function evaluate(
|
|
105
|
+
oldLines: readonly string[],
|
|
106
|
+
newLines: readonly string[],
|
|
107
|
+
indexChanged: boolean,
|
|
108
|
+
overridden: boolean,
|
|
109
|
+
): CheckResult {
|
|
110
|
+
const addedMajors = added_released_breaking_majors(oldLines, newLines);
|
|
111
|
+
const violation = addedMajors.length > 0 && !indexChanged && !overridden;
|
|
112
|
+
return { addedMajors, indexChanged, overridden, violation };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── git helpers (CLI only) ──────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function _git(...args: string[]): string {
|
|
118
|
+
const proc = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf-8' });
|
|
119
|
+
return (proc.stdout as string | null) ?? '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _base(): string {
|
|
123
|
+
const mb = _git('merge-base', BASE_REF, 'HEAD').trim();
|
|
124
|
+
return mb || BASE_REF;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _splitlines(s: string): string[] {
|
|
128
|
+
return s.split(/\r\n|\r|\n/);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** File contents at a ref, or '' when the path did not exist there. */
|
|
132
|
+
function _blob_at(ref: string, rel: string): string {
|
|
133
|
+
return _git('show', `${ref}:${rel}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── selftest fixtures ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function _changelog(sections: string): string[] {
|
|
139
|
+
return ('# Changelog\n\n## [Unreleased]\n\n' + sections).split('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const _RELEASED_WITH_BREAKING =
|
|
143
|
+
'## [9.0.0](https://example/compare/8.13.0...9.0.0) (2026-07-13)\n\n' +
|
|
144
|
+
'### BREAKING CHANGES\n\n* installer path changed ([abc1234](https://example))\n\n';
|
|
145
|
+
const _RELEASED_NO_BREAKING =
|
|
146
|
+
'## [9.0.0](https://example/compare/8.13.0...9.0.0) (2026-07-13)\n\n' +
|
|
147
|
+
'### Features\n\n* a feature ([abc1234](https://example))\n\n';
|
|
148
|
+
|
|
149
|
+
function run_selftest(): number {
|
|
150
|
+
let failed = 0;
|
|
151
|
+
const assert = (label: string, ok: boolean): void => {
|
|
152
|
+
process.stdout.write(`${ok ? '✅' : '❌'} selftest: ${label}\n`);
|
|
153
|
+
if (!ok) failed += 1;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const oldNoBreaking = _changelog(_RELEASED_NO_BREAKING);
|
|
157
|
+
const newWithBreaking = _changelog(_RELEASED_WITH_BREAKING);
|
|
158
|
+
|
|
159
|
+
// 1. Released BREAKING added, index NOT changed, no override → VIOLATION.
|
|
160
|
+
const red = evaluate(oldNoBreaking, newWithBreaking, false, false);
|
|
161
|
+
assert(
|
|
162
|
+
'released BREAKING added without index change FAILS',
|
|
163
|
+
red.violation && red.addedMajors.length === 1 && red.addedMajors[0] === '9.0.0',
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// 2. Same, but the index WAS changed → passes.
|
|
167
|
+
const greenIdx = evaluate(oldNoBreaking, newWithBreaking, true, false);
|
|
168
|
+
assert('index change clears the gate', !greenIdx.violation && greenIdx.addedMajors.length === 1);
|
|
169
|
+
|
|
170
|
+
// 3. Same, index untouched, but override present → passes.
|
|
171
|
+
const greenOverride = evaluate(oldNoBreaking, newWithBreaking, false, true);
|
|
172
|
+
assert('override clears the gate', !greenOverride.violation);
|
|
173
|
+
|
|
174
|
+
// 4. BREAKING only under [Unreleased] (WIP) → never trips.
|
|
175
|
+
const oldEmpty = _changelog('');
|
|
176
|
+
const newUnreleasedBreaking = (
|
|
177
|
+
'# Changelog\n\n## [Unreleased]\n\n### BREAKING CHANGES\n\n* wip break ([abc1234](https://example))\n\n'
|
|
178
|
+
).split('\n');
|
|
179
|
+
const wip = evaluate(oldEmpty, newUnreleasedBreaking, false, false);
|
|
180
|
+
assert('Unreleased BREAKING does not trip the gate', !wip.violation && wip.addedMajors.length === 0);
|
|
181
|
+
|
|
182
|
+
// 5. A released BREAKING already present at base (no new addition) → clean.
|
|
183
|
+
const noop = evaluate(newWithBreaking, newWithBreaking, false, false);
|
|
184
|
+
assert('pre-existing released BREAKING is not re-flagged', !noop.violation);
|
|
185
|
+
|
|
186
|
+
if (failed > 0) {
|
|
187
|
+
process.stderr.write(`❌ selftest: ${failed} fixture(s) failed.\n`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write('✅ selftest: all fixtures behave as specified.\n');
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
interface ParsedArgs {
|
|
197
|
+
selftest: boolean;
|
|
198
|
+
quiet: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _argparse_error(message: string): never {
|
|
202
|
+
process.stderr.write(`lint_breaking_changes_index: error: ${message}\n`);
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parse_args(argv: readonly string[]): ParsedArgs {
|
|
207
|
+
const args: ParsedArgs = { selftest: false, quiet: false };
|
|
208
|
+
for (const arg of argv) {
|
|
209
|
+
if (arg === '--selftest') {
|
|
210
|
+
args.selftest = true;
|
|
211
|
+
} else if (arg === '--quiet') {
|
|
212
|
+
args.quiet = true;
|
|
213
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
214
|
+
process.stdout.write('usage: lint_breaking_changes_index [-h] [--selftest] [--quiet]\n');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
} else {
|
|
217
|
+
_argparse_error(`unrecognized arguments: ${arg}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return args;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function main(argv?: readonly string[]): number {
|
|
224
|
+
const args = parse_args(argv ?? process.argv.slice(2));
|
|
225
|
+
if (args.selftest) {
|
|
226
|
+
return run_selftest();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const base = _base();
|
|
230
|
+
if (!base) {
|
|
231
|
+
return 0; // no trunk to diff against (e.g. shallow CI) → no-op
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const oldLines = _splitlines(_blob_at(base, CHANGELOG_REL));
|
|
235
|
+
const changelogPath = path.join(REPO_ROOT, CHANGELOG_REL);
|
|
236
|
+
const newLines = fs.existsSync(changelogPath)
|
|
237
|
+
? _splitlines(fs.readFileSync(changelogPath, 'utf-8'))
|
|
238
|
+
: [];
|
|
239
|
+
|
|
240
|
+
// Was BREAKING_CHANGES.md touched (staged or unstaged) vs the base?
|
|
241
|
+
const indexChanged = _git('diff', '--name-only', base, '--', INDEX_REL).trim() !== '';
|
|
242
|
+
|
|
243
|
+
const log = _git('log', '--format=%B', `${base}..HEAD`);
|
|
244
|
+
const overridden = OVERRIDE_RE.test(log) || process.env['BREAKING_INDEX_OVERRIDE'] === '1';
|
|
245
|
+
|
|
246
|
+
const result = evaluate(oldLines, newLines, indexChanged, overridden);
|
|
247
|
+
|
|
248
|
+
if (!result.violation) {
|
|
249
|
+
if (!args.quiet) {
|
|
250
|
+
if (result.addedMajors.length > 0) {
|
|
251
|
+
const why = result.overridden ? 'override' : `${INDEX_REL} updated`;
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
`✅ lint-breaking-changes-index: ${result.addedMajors.length} released ` +
|
|
254
|
+
`BREAKING addition(s), cleared by ${why}\n`,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
process.stdout.write(
|
|
258
|
+
'✅ lint-breaking-changes-index: no new released BREAKING entries vs trunk\n',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
process.stderr.write(
|
|
266
|
+
`❌ lint-breaking-changes-index: released BREAKING entr(y/ies) added to ${CHANGELOG_REL} ` +
|
|
267
|
+
`without a matching ${INDEX_REL} change:\n`,
|
|
268
|
+
);
|
|
269
|
+
for (const v of result.addedMajors) {
|
|
270
|
+
process.stderr.write(` - ## [${v}] gained a ### BREAKING section\n`);
|
|
271
|
+
}
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
`\nWhen a released major ships a breaking change, ${INDEX_REL} — the discoverable\n` +
|
|
274
|
+
`index (one entry per major) — must be updated in the same change. Resolve by:\n` +
|
|
275
|
+
` • adding the matching entry to ${INDEX_REL}, or\n` +
|
|
276
|
+
' • adding an own-line `ci-override: breaking-index-override` trailer to a commit\n' +
|
|
277
|
+
' body (or setting BREAKING_INDEX_OVERRIDE=1) for an intentionally deferred index.\n',
|
|
278
|
+
);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function _isCliEntry(): boolean {
|
|
283
|
+
if (process.argv[1] === undefined) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
287
|
+
if (import.meta.url === argvUrl) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
292
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
293
|
+
return here === argv;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
300
|
+
process.exit(main());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { main, run_selftest, RELEASED_MAJOR_HEADING_RE, BREAKING_SUBHEADING_RE, OVERRIDE_RE };
|
|
@@ -321,9 +321,51 @@ function _pyRepr(s: string): string {
|
|
|
321
321
|
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// ── HANDOFF.md artifact validation (workflow-contracts Phase 2) ─────────
|
|
325
|
+
// SEPARATE concern from the cross-wing skill-handoff lint above: this
|
|
326
|
+
// validates the OPTIONAL standing workflow-resume artifact
|
|
327
|
+
// (agents/runtime/state/HANDOFF.md, written by /agent-handoff --file).
|
|
328
|
+
// Same file on purpose — one linter owns everything named "handoff";
|
|
329
|
+
// the naming reconciliation is this comment.
|
|
330
|
+
|
|
331
|
+
export const HANDOFF_ARTIFACT_REQUIRED = [
|
|
332
|
+
'Mode',
|
|
333
|
+
'Contract received',
|
|
334
|
+
'Contract owed',
|
|
335
|
+
'Decisions',
|
|
336
|
+
'Open questions',
|
|
337
|
+
'Next command',
|
|
338
|
+
] as const;
|
|
339
|
+
|
|
340
|
+
/** Returns missing required section headings ([] = valid). */
|
|
341
|
+
export function validate_handoff_artifact(text: string): string[] {
|
|
342
|
+
const missing: string[] = [];
|
|
343
|
+
for (const heading of HANDOFF_ARTIFACT_REQUIRED) {
|
|
344
|
+
const re = new RegExp(`^##\\s+${heading}\\s*$`, 'mi');
|
|
345
|
+
if (!re.test(text)) {
|
|
346
|
+
missing.push(heading);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return missing;
|
|
350
|
+
}
|
|
351
|
+
|
|
324
352
|
export function main(argv?: readonly string[]): number {
|
|
325
353
|
let skills_dir = SKILLS_DIR;
|
|
326
354
|
const args = argv ?? process.argv.slice(2);
|
|
355
|
+
if (args.length > 0 && String(args[0]).endsWith('HANDOFF.md')) {
|
|
356
|
+
// artifact mode — validate the workflow-resume file's required fields
|
|
357
|
+
const text = fs.readFileSync(_resolve(args[0] as string), 'utf-8');
|
|
358
|
+
const missing = validate_handoff_artifact(text);
|
|
359
|
+
if (missing.length === 0) {
|
|
360
|
+
if (!QUIET) process.stdout.write('✅ HANDOFF artifact: all required fields present\n');
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
for (const m of missing) {
|
|
364
|
+
process.stdout.write(`${args[0]}:1:handoff-artifact-missing-field: missing required section \`## ${m}\`\n`);
|
|
365
|
+
}
|
|
366
|
+
process.stderr.write(`❌ ${missing.length} missing HANDOFF field(s)\n`);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
327
369
|
if (args.length > 0) {
|
|
328
370
|
skills_dir = _resolve(args[0] as string);
|
|
329
371
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
/**
|
|
3
|
+
* lint_skill_descriptions — description-quality gate (ecosystem-harvest
|
|
4
|
+
* skill-quality-gates, Phase 1).
|
|
5
|
+
*
|
|
6
|
+
* Description-driven routing is the suite's core asset: the agent picks a skill
|
|
7
|
+
* from its `description`. A circular or condition-free description ("Triggers on
|
|
8
|
+
* X, X" / a description that is just the skill name) makes routing a coin-flip.
|
|
9
|
+
* This linter rejects the four description defects that break routing:
|
|
10
|
+
*
|
|
11
|
+
* (a) desc-equals-name — normalized description ≡ normalized name
|
|
12
|
+
* (b) duplicated-trigger — the same multi-word trigger phrase repeated verbatim
|
|
13
|
+
* (c) triggers-are-name — every trigger phrase is a substring of the name
|
|
14
|
+
* (no new routing signal beyond the name)
|
|
15
|
+
* (d) no-routing-signal — the description gives the router nothing to
|
|
16
|
+
* disambiguate on: no condition marker (when / before /
|
|
17
|
+
* after / only / for …), no specifics enumeration
|
|
18
|
+
* (em-dash / colon / comma list), no quoted example,
|
|
19
|
+
* no imperative-verb lead. A bare topic restatement.
|
|
20
|
+
*
|
|
21
|
+
* (d) is signal-ABSENCE, not a phrase mandate: an imperative house-style
|
|
22
|
+
* description ("Writes Laravel PHP — Eloquent, Artisan …") carries routing
|
|
23
|
+
* signal and passes. The farm-generator's name-echo frontmatter is the
|
|
24
|
+
* must-fail specimen.
|
|
25
|
+
*
|
|
26
|
+
* Deterministic, static-config only. Allowlist mirrors lint_skill_originality:
|
|
27
|
+
* `lint_skill_descriptions_allowlist.json`, capped at 20 (autonomous-execution
|
|
28
|
+
* allowlist-growth antipattern — over-cap means the linter is wrong).
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 = clean (or only allowlisted), 1 = at least one non-allowlisted
|
|
31
|
+
* violation, 2 = allowlist over cap / usage error.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* ./scripts-run src/scripts/lint_skill_descriptions [--quiet] [--json <path>] [--root <dir>]
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import process from 'node:process';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import { parse_frontmatter } from './skill_overlap.js';
|
|
43
|
+
|
|
44
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
45
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
46
|
+
const DEFAULT_SKILLS = path.join(REPO, 'src', 'skills');
|
|
47
|
+
const ALLOWLIST = path.join(path.dirname(_HERE), 'lint_skill_descriptions_allowlist.json');
|
|
48
|
+
const ALLOWLIST_CAP = 20;
|
|
49
|
+
|
|
50
|
+
// A condition / trigger marker — one of the ways a description signals WHEN it routes.
|
|
51
|
+
const CONDITION_RE =
|
|
52
|
+
/\b(use when|use to|use for|use on|use during|when |before |after |only when|only for|triggers on|fires when|activ|for [a-z])/i;
|
|
53
|
+
// An imperative / gerund lead verb — the house-style "Writes X", "Convert X" form
|
|
54
|
+
// carries WHAT-it-does routing signal even without a "when" clause.
|
|
55
|
+
const IMPERATIVE_LEAD_RE =
|
|
56
|
+
/^["'`]*\s*(write|author|convert|review|run|add|build|generate|create|fix|wire|detect|analy[sz]e|optimi[sz]e|draft|extract|route|turn|produce|score|consolidate|rate|model|estimate|plan|check|audit|ship|emit|render|guide|poll|capture|explain|map|prepare|handle|manage|track|compare|validate|verify|enforce|choose|pick|enumerate|investigate|format|scaffold|design|define|describe|gate|block|refactor|migrate|transcribe|summari[sz]e|orchestrate|surface|propose|walk|stress-test|red-team|challenge|bootstrap|seed|install|deploy|package|split|merge|condense|harvest|evaluate|assess|diagnose|reproduce|trace|inspect|lint|test)/i;
|
|
57
|
+
|
|
58
|
+
interface Violation {
|
|
59
|
+
slug: string;
|
|
60
|
+
code: 'desc-equals-name' | 'duplicated-trigger' | 'triggers-are-name' | 'no-routing-signal';
|
|
61
|
+
detail: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A description carries routing signal if it names a condition, leads with an
|
|
65
|
+
* imperative verb, enumerates specifics (em-dash / colon / ≥2 comma segments),
|
|
66
|
+
* or embeds an example query (a quoted phrase). Absence of all four is the defect. */
|
|
67
|
+
function hasRoutingSignal(desc: string): boolean {
|
|
68
|
+
if (CONDITION_RE.test(desc)) return true;
|
|
69
|
+
if (IMPERATIVE_LEAD_RE.test(desc.trim())) return true;
|
|
70
|
+
if (/[—–:;→]/.test(desc)) return true; // specifics enumeration
|
|
71
|
+
if (/['"][^'"]{3,}['"]/.test(desc)) return true; // quoted example query
|
|
72
|
+
if (desc.split(',').filter((s) => s.trim().length >= 3).length >= 2) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalize(s: string): string {
|
|
77
|
+
return s
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[`"'*_.,:;!?()[\]{}]/g, ' ')
|
|
80
|
+
.replace(/[-\s]+/g, ' ')
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Split a description into candidate trigger phrases on sentence/list punctuation. */
|
|
85
|
+
function triggerPhrases(desc: string): string[] {
|
|
86
|
+
return desc
|
|
87
|
+
.split(/[.;·,|]|\bor\b|\band\b|\/|—|–/i)
|
|
88
|
+
.map((p) => normalize(p))
|
|
89
|
+
.filter((p) => p.length >= 3);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function analyseSkill(slug: string, fm: Record<string, string>): Violation[] {
|
|
93
|
+
const out: Violation[] = [];
|
|
94
|
+
const name = fm['name'] ?? slug;
|
|
95
|
+
const desc = (fm['description'] ?? '').trim();
|
|
96
|
+
if (desc === '') {
|
|
97
|
+
// Empty description is a frontmatter-schema failure, not this linter's job.
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
const nName = normalize(name);
|
|
101
|
+
const nDesc = normalize(desc);
|
|
102
|
+
|
|
103
|
+
// (a) description ≡ name
|
|
104
|
+
if (nDesc === nName) {
|
|
105
|
+
out.push({ slug, code: 'desc-equals-name', detail: `description normalizes to the name "${name}"` });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const phrases = triggerPhrases(desc);
|
|
109
|
+
|
|
110
|
+
// (b) duplicated MULTI-WORD trigger phrase repeated verbatim (the farm-specimen
|
|
111
|
+
// "Triggers on: X, X" defect) — a single recurring domain word is not a defect.
|
|
112
|
+
const multiWord = phrases.filter((p) => p.includes(' '));
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
for (const p of multiWord) {
|
|
115
|
+
if (seen.has(p)) {
|
|
116
|
+
out.push({ slug, code: 'duplicated-trigger', detail: `trigger phrase repeated verbatim: "${p}"` });
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
seen.add(p);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// (c) every trigger phrase is a substring of the name (no signal beyond name)
|
|
123
|
+
if (phrases.length > 0 && phrases.every((p) => nName.includes(p) || p.includes(nName))) {
|
|
124
|
+
out.push({
|
|
125
|
+
slug,
|
|
126
|
+
code: 'triggers-are-name',
|
|
127
|
+
detail: 'every trigger phrase is contained in the skill name — no routing signal beyond the name',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// (d) no routing signal at all — bare topic restatement
|
|
132
|
+
if (!hasRoutingSignal(desc)) {
|
|
133
|
+
out.push({
|
|
134
|
+
slug,
|
|
135
|
+
code: 'no-routing-signal',
|
|
136
|
+
detail: 'no condition marker, imperative-verb lead, specifics enumeration, or example query — the router has nothing to disambiguate on',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function loadAllowlist(): Set<string> {
|
|
144
|
+
if (!fs.existsSync(ALLOWLIST)) {
|
|
145
|
+
return new Set();
|
|
146
|
+
}
|
|
147
|
+
const data = JSON.parse(fs.readFileSync(ALLOWLIST, 'utf-8')) as {
|
|
148
|
+
entries?: Array<{ slug: string; code: string }>;
|
|
149
|
+
};
|
|
150
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
151
|
+
if (entries.length > ALLOWLIST_CAP) {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`❌ lint_skill_descriptions: allowlist has ${entries.length} entries (> ${ALLOWLIST_CAP}). ` +
|
|
154
|
+
`Per the autonomous-execution allowlist-growth antipattern, the linter is wrong, ` +
|
|
155
|
+
`not the content — tighten the heuristic, do not grow the allowlist.\n`,
|
|
156
|
+
);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
return new Set(entries.map((e) => `${e.slug}::${e.code}`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function* skillMds(root: string): Generator<string> {
|
|
163
|
+
if (!fs.existsSync(root)) return;
|
|
164
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
165
|
+
if (!entry.isDirectory()) continue;
|
|
166
|
+
const md = path.join(root, entry.name, 'SKILL.md');
|
|
167
|
+
if (fs.existsSync(md)) yield md;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface Args {
|
|
172
|
+
quiet: boolean;
|
|
173
|
+
json: string | null;
|
|
174
|
+
root: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseArgs(argv: string[]): Args {
|
|
178
|
+
const a: Args = { quiet: false, json: null, root: DEFAULT_SKILLS };
|
|
179
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
180
|
+
const arg = argv[i];
|
|
181
|
+
if (arg === '--quiet') a.quiet = true;
|
|
182
|
+
else if (arg === '--json') a.json = argv[(i += 1)] ?? '';
|
|
183
|
+
else if (arg === '--root') a.root = argv[(i += 1)] ?? DEFAULT_SKILLS;
|
|
184
|
+
else {
|
|
185
|
+
process.stderr.write(`lint_skill_descriptions: unrecognized argument: ${arg}\n`);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return a;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function main(argv: string[]): number {
|
|
193
|
+
const args = parseArgs(argv);
|
|
194
|
+
const allow = loadAllowlist();
|
|
195
|
+
const violations: Violation[] = [];
|
|
196
|
+
let scanned = 0;
|
|
197
|
+
for (const md of skillMds(args.root)) {
|
|
198
|
+
scanned += 1;
|
|
199
|
+
const slug = path.basename(path.dirname(md));
|
|
200
|
+
const [fm] = parse_frontmatter(fs.readFileSync(md, 'utf-8'));
|
|
201
|
+
for (const v of analyseSkill(slug, fm)) {
|
|
202
|
+
if (!allow.has(`${v.slug}::${v.code}`)) violations.push(v);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (args.json) {
|
|
207
|
+
fs.mkdirSync(path.dirname(args.json), { recursive: true });
|
|
208
|
+
fs.writeFileSync(args.json, JSON.stringify({ scanned, violations }, null, 2) + '\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (violations.length > 0) {
|
|
212
|
+
process.stderr.write(`❌ lint_skill_descriptions: ${violations.length} description defect(s):\n`);
|
|
213
|
+
for (const v of violations) {
|
|
214
|
+
process.stderr.write(` ${v.slug} [${v.code}] — ${v.detail}\n`);
|
|
215
|
+
}
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
if (!args.quiet) {
|
|
219
|
+
process.stdout.write(`✅ lint_skill_descriptions: ${scanned} skill(s) scanned, no description defects.\n`);
|
|
220
|
+
}
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
|
|
225
|
+
process.exit(main(process.argv.slice(2)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { analyseSkill, normalize, triggerPhrases, main };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "entries": [] }
|