@event4u/agent-config 8.13.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +136 -0
- package/README.md +1 -1
- 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/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 +1 -0
- package/dist/agent-src/commands/pr/create.md +19 -17
- 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/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/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/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/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +256 -190
- 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 +20 -17
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +16 -13
- package/dist/install/install.mjs +10 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +5 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +5 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +8 -1
- package/docs/SKILL_CENSUS.md +8 -0
- package/docs/architecture.md +2 -2
- package/docs/benchmark.md +67 -2
- package/docs/catalog.md +2 -2
- package/docs/contracts/ai-council-config.md +9 -2
- package/docs/contracts/ai-team-config.md +37 -4
- package/docs/contracts/prelaunch-diagnostics.md +151 -0
- package/docs/contracts/release-pr-gating.md +1 -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 +16 -5
- 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/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/parity/command-frontmatter.md +60 -0
- package/docs/proof.md +6 -5
- 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/config/agent-settings.template.yml +34 -10
- package/src/config/prelaunch-areas.yml +36 -0
- package/src/scripts/_cli/cmd_doctor.ts +118 -26
- 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_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/hook_manifest.yaml +12 -1
- 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/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/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/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 +9 -1
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* CI guard: council config lives in `.ai-council.yml`, never `.agent-settings.yml`.
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `src/scripts/check_council_config_location.py` (ADR-200).
|
|
6
|
-
* The
|
|
7
|
-
* 1 at least one violation), byte-identical finding
|
|
8
|
-
* (sorted), same fence tracking, same negation /
|
|
9
|
-
* and the same `<!-- council-config-allowed -->`
|
|
10
|
-
*
|
|
6
|
+
* The original two checks keep their pinned CLI contract — `--quiet` flag,
|
|
7
|
+
* exit codes (0 clean, 1 at least one violation), byte-identical finding
|
|
8
|
+
* messages, same `SCAN_GLOBS` (sorted), same fence tracking, same negation /
|
|
9
|
+
* `ai_council:` block detection, and the same `<!-- council-config-allowed -->`
|
|
10
|
+
* escape pragma.
|
|
11
|
+
*
|
|
12
|
+
* Additive since ADR-104: a third check (§3 below) flags project-tree
|
|
13
|
+
* *placement* of the council file. The original two checks are unchanged; the
|
|
14
|
+
* path check runs over its own `PATH_CHECK_GLOBS` (the council surfaces + the
|
|
15
|
+
* settings template + the `.ai-council.yml.example`) so the settings template's
|
|
16
|
+
* many legitimate `.agent-settings.yml` self-references never trip check §1.
|
|
11
17
|
*
|
|
12
18
|
* Per ADR-104 (superseding ADR-093) the council reads a dedicated
|
|
13
19
|
* `.ai-council.yml` resolved ALWAYS from the user-global location
|
|
@@ -16,19 +22,26 @@
|
|
|
16
22
|
* path). Keys are top-level in that file; the legacy `ai_council.*` block
|
|
17
23
|
* under `.agent-settings.yml` was removed in Phase 0.
|
|
18
24
|
*
|
|
19
|
-
* What it flags
|
|
25
|
+
* What it flags:
|
|
20
26
|
*
|
|
21
27
|
* 1. A `.agent-settings.yml` reference that is NOT negated — i.e. an
|
|
22
28
|
* instruction to read/use it for council config. Corrective mentions
|
|
23
29
|
* ("NOT in `.agent-settings.yml`", "was removed", "never read") carry a
|
|
24
|
-
* negation marker on the same line and are allowed.
|
|
30
|
+
* negation marker on the same line and are allowed. (SCAN_GLOBS)
|
|
25
31
|
* 2. A bare `ai_council:` YAML parent-block declaration — post-ADR-093 the
|
|
26
32
|
* keys are top-level in `.ai-council.yml`; there is no `ai_council:`
|
|
27
|
-
* namespace to nest under.
|
|
33
|
+
* namespace to nest under. (SCAN_GLOBS)
|
|
34
|
+
* 3. A project-tree PLACEMENT of the council file — `agents/.ai-council.yml`,
|
|
35
|
+
* `agents/settings/.ai-council.yml`, or a `<project_root>/…/.ai-council.yml`
|
|
36
|
+
* path — that is NOT negated. The council file is user-global ONLY
|
|
37
|
+
* (ADR-104); any project-tree path is drift. Corrective mentions ("is
|
|
38
|
+
* ignored", "never read", "no project-local", "superseded") pass on the
|
|
39
|
+
* same line. (PATH_CHECK_GLOBS)
|
|
28
40
|
*
|
|
29
41
|
* Escape hatch: a line carrying `<!-- council-config-allowed -->` is exempt
|
|
30
42
|
* (for a legitimate non-council `.agent-settings.yml` reference, e.g.
|
|
31
|
-
* `personal.autonomy
|
|
43
|
+
* `personal.autonomy`, or a historical placement mention in a section
|
|
44
|
+
* explicitly marked superseded).
|
|
32
45
|
*
|
|
33
46
|
* Exit codes:
|
|
34
47
|
* 0 — clean.
|
|
@@ -61,6 +74,35 @@ const NEGATION_RE = /\b(not|never|removed|no\s+longer|neither|instead)\b/i;
|
|
|
61
74
|
const AI_COUNCIL_BLOCK_RE = /^\s*ai_council:\s*(#.*)?$/;
|
|
62
75
|
const ALLOW_PRAGMA = '<!-- council-config-allowed -->';
|
|
63
76
|
|
|
77
|
+
// ── §3 project-tree placement check (ADR-104) ──────────────────────
|
|
78
|
+
// The council file is user-global ONLY. These are the surfaces where a
|
|
79
|
+
// stray project-tree placement instruction does real damage — the council
|
|
80
|
+
// command/skill surfaces + contract (SCAN_GLOBS) PLUS the settings template
|
|
81
|
+
// and the copy-from example. The template carries countless legitimate
|
|
82
|
+
// `.agent-settings.yml` self-references, so it is scanned ONLY by §3, never
|
|
83
|
+
// by §1.
|
|
84
|
+
const PATH_CHECK_GLOBS = [
|
|
85
|
+
...SCAN_GLOBS,
|
|
86
|
+
'src/config/agent-settings.template.yml',
|
|
87
|
+
'agents/templates/.ai-council.yml.example',
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
// A project-tree PLACEMENT of the council file: `agents/.ai-council.yml`,
|
|
91
|
+
// `agents/settings/.ai-council.yml`, or any `<project_root>/…/.ai-council.yml`.
|
|
92
|
+
// Deliberately does NOT match the user-global `…/agent-config/settings/
|
|
93
|
+
// .ai-council.yml` (no literal `agents/` segment) nor the copy-from
|
|
94
|
+
// `agents/templates/.ai-council.yml.example` (segment is `templates/`, and
|
|
95
|
+
// `.example` is not the config file).
|
|
96
|
+
const PROJECT_PATH_COUNCIL_RE =
|
|
97
|
+
/(?:agents\/(?:settings\/)?\.ai-council\.yml(?!\.example)|<project[_-]?root>[^\n`]*\.ai-council\.yml)/;
|
|
98
|
+
|
|
99
|
+
// Broader negation set for §3 — corrective placement mentions use vocabulary
|
|
100
|
+
// the §1 NEGATION_RE does not cover ("is ignored", "no project-local
|
|
101
|
+
// lookup", "superseded"). A match on the same line marks the reference
|
|
102
|
+
// corrective and is allowed.
|
|
103
|
+
const PATH_NEGATION_RE =
|
|
104
|
+
/\b(not|never|removed|no\s+longer|neither|instead|ignored|superseded|no\s+project-local|not\s+read|does\s+not\s+read)\b/i;
|
|
105
|
+
|
|
64
106
|
/**
|
|
65
107
|
* Mirror Python `str.lstrip()` — strips all leading Unicode whitespace.
|
|
66
108
|
* Python's `str.lstrip()` (no args) strips the Unicode whitespace class;
|
|
@@ -79,9 +121,12 @@ function _lstrip(s: string): string {
|
|
|
79
121
|
* the same sort (component-wise via Path comparison). We approximate with a
|
|
80
122
|
* recursive walk per glob pattern, sorting POSIX paths.
|
|
81
123
|
*/
|
|
82
|
-
function* iter_files(
|
|
124
|
+
function* iter_files(
|
|
125
|
+
root: string,
|
|
126
|
+
globs: readonly string[] = SCAN_GLOBS,
|
|
127
|
+
): Generator<string> {
|
|
83
128
|
const seen = new Set<string>();
|
|
84
|
-
for (const pattern of
|
|
129
|
+
for (const pattern of globs) {
|
|
85
130
|
for (const p of _glob(root, pattern)) {
|
|
86
131
|
if (_isFile(p) && !seen.has(p)) {
|
|
87
132
|
seen.add(p);
|
|
@@ -220,18 +265,69 @@ function find_violations(root: string): string[] {
|
|
|
220
265
|
return findings;
|
|
221
266
|
}
|
|
222
267
|
|
|
268
|
+
/**
|
|
269
|
+
* §3 — flag project-tree PLACEMENT of the council config file (ADR-104).
|
|
270
|
+
*
|
|
271
|
+
* The council file is user-global only; any `agents/(settings/)?.ai-council.yml`
|
|
272
|
+
* or `<project_root>/…/.ai-council.yml` path is drift. Corrective mentions
|
|
273
|
+
* (same-line `PATH_NEGATION_RE`) and the `<!-- council-config-allowed -->`
|
|
274
|
+
* pragma pass. Fenced code is skipped, mirroring §1/§2.
|
|
275
|
+
*/
|
|
276
|
+
function find_path_violations(root: string): string[] {
|
|
277
|
+
const findings: string[] = [];
|
|
278
|
+
for (const p of iter_files(root, PATH_CHECK_GLOBS)) {
|
|
279
|
+
const rel = _relPosix(root, p);
|
|
280
|
+
let in_fence = false;
|
|
281
|
+
const lines = fs.readFileSync(p, 'utf-8').split('\n');
|
|
282
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
283
|
+
lines.pop();
|
|
284
|
+
}
|
|
285
|
+
for (let i = 0; i < lines.length; i++) {
|
|
286
|
+
const lineno = i + 1;
|
|
287
|
+
const raw = lines[i]!;
|
|
288
|
+
const stripped = _lstrip(raw);
|
|
289
|
+
if (stripped.startsWith('```') || stripped.startsWith('~~~')) {
|
|
290
|
+
in_fence = !in_fence;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (in_fence || raw.includes(ALLOW_PRAGMA)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Corrective sentences often wrap, leaving the negation
|
|
297
|
+
// ("never reads", "no project-local lookup") on the previous
|
|
298
|
+
// line and the path on this one. Check both lines for negation
|
|
299
|
+
// so a correctly-negated wrapped mention is not flagged. A real
|
|
300
|
+
// "place it here" instruction never carries a negation one line up.
|
|
301
|
+
const prev = i > 0 ? lines[i - 1]! : '';
|
|
302
|
+
const negated =
|
|
303
|
+
PATH_NEGATION_RE.test(raw) || PATH_NEGATION_RE.test(prev);
|
|
304
|
+
if (PROJECT_PATH_COUNCIL_RE.test(raw) && !negated) {
|
|
305
|
+
findings.push(
|
|
306
|
+
`${rel}:${lineno}: council config placed in the project tree ` +
|
|
307
|
+
'— the `.ai-council.yml` file is user-global ONLY ' +
|
|
308
|
+
'(`~/.event4u/agent-config/settings/.ai-council.yml`, ADR-104). ' +
|
|
309
|
+
'The project tree is never searched. Point at the user-global ' +
|
|
310
|
+
'path, add a negation marker, or add ' +
|
|
311
|
+
`\`${ALLOW_PRAGMA}\` for a historical/superseded reference.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return findings;
|
|
317
|
+
}
|
|
318
|
+
|
|
223
319
|
function main(): number {
|
|
224
320
|
const root = process.cwd();
|
|
225
|
-
const findings = find_violations(root);
|
|
321
|
+
const findings = [...find_violations(root), ...find_path_violations(root)];
|
|
226
322
|
if (findings.length) {
|
|
227
323
|
process.stdout.write('❌ Council config-location violations:\n\n');
|
|
228
324
|
for (const f of findings) {
|
|
229
325
|
process.stdout.write(` - ${f}\n`);
|
|
230
326
|
}
|
|
231
327
|
process.stdout.write(
|
|
232
|
-
'\nRule: council config lives in `.ai-council.yml` ' +
|
|
233
|
-
'(
|
|
234
|
-
'`.agent-settings.yml
|
|
328
|
+
'\nRule: council config lives in the user-global `.ai-council.yml` ' +
|
|
329
|
+
'(`~/.event4u/agent-config/settings/.ai-council.yml`, ADR-104) — ' +
|
|
330
|
+
'never in `.agent-settings.yml`, never in a project `agents/` tree.\n',
|
|
235
331
|
);
|
|
236
332
|
return 1;
|
|
237
333
|
}
|
|
@@ -270,11 +366,15 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
270
366
|
|
|
271
367
|
export {
|
|
272
368
|
SCAN_GLOBS,
|
|
369
|
+
PATH_CHECK_GLOBS,
|
|
273
370
|
AGENT_SETTINGS_RE,
|
|
274
371
|
NEGATION_RE,
|
|
275
372
|
AI_COUNCIL_BLOCK_RE,
|
|
373
|
+
PROJECT_PATH_COUNCIL_RE,
|
|
374
|
+
PATH_NEGATION_RE,
|
|
276
375
|
ALLOW_PRAGMA,
|
|
277
376
|
iter_files,
|
|
278
377
|
find_violations,
|
|
378
|
+
find_path_violations,
|
|
279
379
|
main,
|
|
280
380
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check_host_loadability.ts — U4 real-host loadability smoke
|
|
3
|
+
* (road-to-ecosystem-harvest-reliability-measurement).
|
|
4
|
+
*
|
|
5
|
+
* The condensation-hash + linter gates prove the SOURCE tree's shape; this
|
|
6
|
+
* check proves the GENERATED host trees are actually loadable by their hosts:
|
|
7
|
+
* - .claude/skills/<dir>/SKILL.md — frontmatter parses, has name +
|
|
8
|
+
* description, and name matches the directory (Claude Code's load rule).
|
|
9
|
+
* - .cursor/rules/*.mdc — frontmatter parses (Cursor rejects the file
|
|
10
|
+
* silently otherwise).
|
|
11
|
+
* Run AFTER the projection is generated (CI: the sync-consistency job).
|
|
12
|
+
* Exit 1 on the first malformed artefact, naming it.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { parse as parseYaml } from 'yaml';
|
|
18
|
+
|
|
19
|
+
export function check_claude_skills(root: string, errors: string[]): void {
|
|
20
|
+
const dir = path.join(root, '.claude', 'skills');
|
|
21
|
+
if (!fs.existsSync(dir)) return;
|
|
22
|
+
for (const name of fs.readdirSync(dir)) {
|
|
23
|
+
const skillMd = path.join(dir, name, 'SKILL.md');
|
|
24
|
+
if (!fs.existsSync(skillMd)) {
|
|
25
|
+
errors.push(`${skillMd}: missing SKILL.md`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const src = fs.readFileSync(skillMd, 'utf-8');
|
|
29
|
+
const m = src.match(/^---\n([\s\S]*?)\n---/);
|
|
30
|
+
if (!m) { errors.push(`${skillMd}: no frontmatter`); continue; }
|
|
31
|
+
let meta: Record<string, unknown>;
|
|
32
|
+
try { meta = parseYaml(m[1] ?? '') as Record<string, unknown>; }
|
|
33
|
+
catch (e) { errors.push(`${skillMd}: frontmatter YAML invalid (${(e as Error).message})`); continue; }
|
|
34
|
+
if (typeof meta['name'] !== 'string' || !meta['name']) errors.push(`${skillMd}: missing name`);
|
|
35
|
+
else if (meta['name'] !== name) errors.push(`${skillMd}: name '${meta['name']}' != dir '${name}'`);
|
|
36
|
+
if (typeof meta['description'] !== 'string' || !meta['description']) errors.push(`${skillMd}: missing description`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function check_cursor_rules(root: string, errors: string[]): void {
|
|
41
|
+
const dir = path.join(root, '.cursor', 'rules');
|
|
42
|
+
if (!fs.existsSync(dir)) return;
|
|
43
|
+
for (const name of fs.readdirSync(dir)) {
|
|
44
|
+
if (!name.endsWith('.mdc')) continue;
|
|
45
|
+
const p = path.join(dir, name);
|
|
46
|
+
const src = fs.readFileSync(p, 'utf-8');
|
|
47
|
+
const m = src.match(/^---\n([\s\S]*?)\n---/);
|
|
48
|
+
if (!m) { errors.push(`${p}: no frontmatter`); continue; }
|
|
49
|
+
// Cursor's documented .mdc format writes `globs: *.php` UNQUOTED —
|
|
50
|
+
// valid for Cursor's parser, invalid strict YAML (`*` starts an
|
|
51
|
+
// alias). Quote the globs value before the strict parse so the check
|
|
52
|
+
// mirrors the host's acceptance, not the YAML spec's.
|
|
53
|
+
const lenient = (m[1] ?? '').replace(
|
|
54
|
+
/^(globs:[ \t]*)([^"'\s[][^\n]*)$/m,
|
|
55
|
+
(_all, key: string, val: string) => `${key}"${val.trim()}"`,
|
|
56
|
+
);
|
|
57
|
+
try { parseYaml(lenient); }
|
|
58
|
+
catch (e) { errors.push(`${p}: frontmatter YAML invalid (${(e as Error).message})`); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function run(root: string): string[] {
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
check_claude_skills(root, errors);
|
|
65
|
+
check_cursor_rules(root, errors);
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isMain = process.argv[1] !== undefined
|
|
70
|
+
&& path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
|
71
|
+
if (isMain) {
|
|
72
|
+
const rootIdx = process.argv.indexOf('--root');
|
|
73
|
+
const root = rootIdx !== -1 ? process.argv[rootIdx + 1] ?? '.' : '.';
|
|
74
|
+
const errors = run(root);
|
|
75
|
+
if (errors.length > 0) {
|
|
76
|
+
for (const e of errors) process.stderr.write(`❌ ${e}\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write('✅ host trees loadable (.claude skills + .cursor mdc)\n');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
package/src/scripts/condense.ts
CHANGED
|
@@ -1670,6 +1670,10 @@ export function generate_claude_subagents(): number {
|
|
|
1670
1670
|
const tools = Array.isArray(toolsRaw) ? toolsRaw.map((t) => String(t)).join(', ') : String(toolsRaw ?? '');
|
|
1671
1671
|
const tier = typeof fm['model_tier'] === 'string' ? (fm['model_tier'] as string) : 'inherit';
|
|
1672
1672
|
const model = tier === 'inherit' ? 'inherit' : (_TIER_TO_CLAUDE_MODEL[tier] ?? 'inherit');
|
|
1673
|
+
// Optional pinned reasoning effort (skill-quality-gates Phase 4, Source S):
|
|
1674
|
+
// pass through to the native frontmatter only when the source declares it —
|
|
1675
|
+
// hosts without an effort knob never see the key.
|
|
1676
|
+
const effort = typeof fm['effort'] === 'string' ? (fm['effort'] as string) : null;
|
|
1673
1677
|
|
|
1674
1678
|
const guardedBody =
|
|
1675
1679
|
defense !== '' && !body.includes('prompt-defense-preamble')
|
|
@@ -1681,6 +1685,7 @@ export function generate_claude_subagents(): number {
|
|
|
1681
1685
|
`description: ${description}\n` +
|
|
1682
1686
|
`tools: ${tools}\n` +
|
|
1683
1687
|
`model: ${model}\n` +
|
|
1688
|
+
(effort !== null ? `effort: ${effort}\n` : '') +
|
|
1684
1689
|
'---\n' +
|
|
1685
1690
|
guardedBody;
|
|
1686
1691
|
|
|
@@ -134,6 +134,17 @@ concerns:
|
|
|
134
134
|
script: src/scripts/surface_probe_hook.ts
|
|
135
135
|
args: []
|
|
136
136
|
fail_closed: false
|
|
137
|
+
# road-to-team-mode Phase 4 — managed Review-Gate circuit breaker.
|
|
138
|
+
# Runs AFTER the plugin's own Stop gate: reads the persisted gate job
|
|
139
|
+
# record, counts consecutive BLOCK verdicts per session, injects the
|
|
140
|
+
# circuit-breaker notice exactly once at ai_team.review_gate.
|
|
141
|
+
# max_consecutive_blocks. No-op (byte-identical Stop path) while
|
|
142
|
+
# ai_team.review_gate.managed is false — the shipped default.
|
|
143
|
+
# Never blocks, exit 0 always.
|
|
144
|
+
team-review-gate:
|
|
145
|
+
script: src/scripts/team_review_gate_hook.ts
|
|
146
|
+
args: []
|
|
147
|
+
fail_closed: false
|
|
137
148
|
|
|
138
149
|
platforms:
|
|
139
150
|
augment:
|
|
@@ -146,7 +157,7 @@ platforms:
|
|
|
146
157
|
claude:
|
|
147
158
|
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
148
159
|
session_end: [chat-history]
|
|
149
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
160
|
+
stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
|
|
150
161
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
151
162
|
pre_tool_use: [block-no-verify, rtk-wrap, design-slop]
|
|
152
163
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
|
|
@@ -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": [] }
|