@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.
Files changed (279) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +136 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/agent-handoff.md +35 -0
  5. package/dist/agent-src/commands/agents/init.md +1 -0
  6. package/dist/agent-src/commands/agents/optimize.md +1 -0
  7. package/dist/agent-src/commands/agents/user/accept.md +3 -2
  8. package/dist/agent-src/commands/agents/user/init.md +5 -4
  9. package/dist/agent-src/commands/agents/user/update.md +3 -2
  10. package/dist/agent-src/commands/agents/user.md +1 -0
  11. package/dist/agent-src/commands/agents.md +1 -0
  12. package/dist/agent-src/commands/analytics/show.md +1 -0
  13. package/dist/agent-src/commands/analytics.md +1 -0
  14. package/dist/agent-src/commands/analyze/decision.md +1 -0
  15. package/dist/agent-src/commands/analyze/incident.md +1 -0
  16. package/dist/agent-src/commands/analyze/near-miss.md +1 -0
  17. package/dist/agent-src/commands/analyze/postmortem.md +1 -0
  18. package/dist/agent-src/commands/analyze/premortem.md +1 -0
  19. package/dist/agent-src/commands/analyze/reference-repo.md +1 -0
  20. package/dist/agent-src/commands/analyze.md +3 -2
  21. package/dist/agent-src/commands/brand/identity.md +6 -5
  22. package/dist/agent-src/commands/brand/review.md +6 -5
  23. package/dist/agent-src/commands/brand/strategy.md +6 -4
  24. package/dist/agent-src/commands/brand/tokens.md +7 -6
  25. package/dist/agent-src/commands/brand/voice.md +5 -4
  26. package/dist/agent-src/commands/brand.md +9 -7
  27. package/dist/agent-src/commands/bug/fix.md +1 -0
  28. package/dist/agent-src/commands/bug/investigate.md +1 -0
  29. package/dist/agent-src/commands/bug.md +2 -1
  30. package/dist/agent-src/commands/challenge-me/vision.md +1 -0
  31. package/dist/agent-src/commands/challenge-me/with-docs.md +1 -0
  32. package/dist/agent-src/commands/challenge-me.md +28 -6
  33. package/dist/agent-src/commands/chat-history.md +1 -0
  34. package/dist/agent-src/commands/check-current-md.md +1 -0
  35. package/dist/agent-src/commands/commit.md +1 -0
  36. package/dist/agent-src/commands/context/create.md +3 -2
  37. package/dist/agent-src/commands/context.md +1 -0
  38. package/dist/agent-src/commands/cost/profile.md +1 -0
  39. package/dist/agent-src/commands/cost.md +2 -1
  40. package/dist/agent-src/commands/council/analysis.md +1 -0
  41. package/dist/agent-src/commands/council/debate.md +1 -0
  42. package/dist/agent-src/commands/council/default.md +21 -20
  43. package/dist/agent-src/commands/council/design.md +1 -0
  44. package/dist/agent-src/commands/council/optimize.md +1 -0
  45. package/dist/agent-src/commands/council/pr.md +1 -0
  46. package/dist/agent-src/commands/council.md +1 -0
  47. package/dist/agent-src/commands/estimate-ticket.md +1 -0
  48. package/dist/agent-src/commands/feature/dev.md +1 -0
  49. package/dist/agent-src/commands/feature/explore.md +1 -0
  50. package/dist/agent-src/commands/feature/plan.md +1 -0
  51. package/dist/agent-src/commands/feature/refactor.md +1 -0
  52. package/dist/agent-src/commands/feature/roadmap.md +1 -0
  53. package/dist/agent-src/commands/feature.md +2 -1
  54. package/dist/agent-src/commands/fix/pr-comments.md +19 -0
  55. package/dist/agent-src/commands/fix.md +2 -1
  56. package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
  57. package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
  58. package/dist/agent-src/commands/ghostwriter/show.md +1 -0
  59. package/dist/agent-src/commands/ghostwriter/write.md +1 -0
  60. package/dist/agent-src/commands/ghostwriter.md +1 -0
  61. package/dist/agent-src/commands/grill-me.md +17 -12
  62. package/dist/agent-src/commands/humanize.md +1 -0
  63. package/dist/agent-src/commands/image/analyse.md +2 -1
  64. package/dist/agent-src/commands/image/create.md +1 -0
  65. package/dist/agent-src/commands/image/verify.md +1 -0
  66. package/dist/agent-src/commands/image.md +16 -12
  67. package/dist/agent-src/commands/implement-ticket.md +1 -0
  68. package/dist/agent-src/commands/judge/on-diff.md +1 -0
  69. package/dist/agent-src/commands/judge/solo.md +1 -0
  70. package/dist/agent-src/commands/judge/steps.md +1 -0
  71. package/dist/agent-src/commands/judge.md +1 -0
  72. package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
  73. package/dist/agent-src/commands/knowledge/forget.md +1 -0
  74. package/dist/agent-src/commands/knowledge/ingest.md +1 -0
  75. package/dist/agent-src/commands/knowledge/list.md +10 -0
  76. package/dist/agent-src/commands/knowledge.md +1 -0
  77. package/dist/agent-src/commands/memory/learn-low-impact.md +1 -0
  78. package/dist/agent-src/commands/memory/mine-session.md +1 -0
  79. package/dist/agent-src/commands/memory/promote.md +3 -2
  80. package/dist/agent-src/commands/memory.md +1 -0
  81. package/dist/agent-src/commands/mission/upgrade.md +45 -24
  82. package/dist/agent-src/commands/mode.md +1 -0
  83. package/dist/agent-src/commands/module/create.md +1 -0
  84. package/dist/agent-src/commands/module/explore.md +1 -0
  85. package/dist/agent-src/commands/module.md +1 -0
  86. package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
  87. package/dist/agent-src/commands/optimize/project.md +1 -0
  88. package/dist/agent-src/commands/optimize/prompt.md +1 -0
  89. package/dist/agent-src/commands/optimize.md +2 -1
  90. package/dist/agent-src/commands/orchestrate.md +1 -0
  91. package/dist/agent-src/commands/override.md +1 -0
  92. package/dist/agent-src/commands/package.md +2 -1
  93. package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
  94. package/dist/agent-src/commands/post-as/me.md +1 -0
  95. package/dist/agent-src/commands/post-as.md +1 -0
  96. package/dist/agent-src/commands/pr/create/description-only.md +1 -0
  97. package/dist/agent-src/commands/pr/create.md +19 -17
  98. package/dist/agent-src/commands/prediction-pool.md +7 -6
  99. package/dist/agent-src/commands/prepare-for-review.md +1 -0
  100. package/dist/agent-src/commands/profile/activate.md +1 -0
  101. package/dist/agent-src/commands/profile/deactivate.md +1 -0
  102. package/dist/agent-src/commands/profile.md +1 -0
  103. package/dist/agent-src/commands/project.md +2 -1
  104. package/dist/agent-src/commands/refine-ticket.md +7 -0
  105. package/dist/agent-src/commands/research/deep.md +1 -0
  106. package/dist/agent-src/commands/research.md +1 -0
  107. package/dist/agent-src/commands/review/routing.md +1 -0
  108. package/dist/agent-src/commands/review.md +2 -1
  109. package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
  110. package/dist/agent-src/commands/roadmap/materialize.md +1 -0
  111. package/dist/agent-src/commands/roadmap/process-full.md +1 -0
  112. package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
  113. package/dist/agent-src/commands/roadmap/process-step.md +1 -0
  114. package/dist/agent-src/commands/roadmap.md +1 -0
  115. package/dist/agent-src/commands/security-audit-config.md +2 -1
  116. package/dist/agent-src/commands/skill/preview.md +1 -0
  117. package/dist/agent-src/commands/skill.md +1 -0
  118. package/dist/agent-src/commands/skills/discover.md +1 -0
  119. package/dist/agent-src/commands/skills.md +1 -0
  120. package/dist/agent-src/commands/sync/agent-settings.md +1 -0
  121. package/dist/agent-src/commands/sync.md +2 -1
  122. package/dist/agent-src/commands/team/adversarial.md +3 -2
  123. package/dist/agent-src/commands/team/delegate.md +3 -2
  124. package/dist/agent-src/commands/team/review.md +3 -2
  125. package/dist/agent-src/commands/team/status.md +49 -18
  126. package/dist/agent-src/commands/team-knowledge.md +8 -6
  127. package/dist/agent-src/commands/team.md +21 -16
  128. package/dist/agent-src/commands/tests/e2e-heal.md +1 -0
  129. package/dist/agent-src/commands/tests/e2e-plan.md +1 -0
  130. package/dist/agent-src/commands/tests/execute.md +1 -0
  131. package/dist/agent-src/commands/tests.md +2 -1
  132. package/dist/agent-src/commands/update-form-request-messages.md +1 -0
  133. package/dist/agent-src/commands/upstream-contribute.md +1 -0
  134. package/dist/agent-src/commands/video/from-script.md +3 -2
  135. package/dist/agent-src/commands/video/from-song.md +22 -21
  136. package/dist/agent-src/commands/video/scene.md +1 -0
  137. package/dist/agent-src/commands/video/stitch.md +2 -1
  138. package/dist/agent-src/commands/video/storyboard.md +1 -0
  139. package/dist/agent-src/commands/video.md +1 -0
  140. package/dist/agent-src/commands/work.md +1 -0
  141. package/dist/agent-src/commands/worktree/cleanup.md +8 -7
  142. package/dist/agent-src/commands/worktree/create.md +1 -0
  143. package/dist/agent-src/commands/worktree/verify.md +1 -0
  144. package/dist/agent-src/commands/worktree.md +1 -0
  145. package/dist/agent-src/rules/active-remediation.md +12 -36
  146. package/dist/agent-src/rules/artifact-drafting-protocol.md +7 -64
  147. package/dist/agent-src/rules/context-hygiene.md +19 -113
  148. package/dist/agent-src/rules/design-fidelity.md +4 -60
  149. package/dist/agent-src/rules/domain-adoption-policy.md +10 -106
  150. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +12 -60
  151. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  152. package/dist/agent-src/rules/minimal-safe-diff.md +6 -86
  153. package/dist/agent-src/rules/size-enforcement.md +10 -0
  154. package/dist/agent-src/rules/tool-safety.md +17 -3
  155. package/dist/agent-src/skills/agent-security-review/SKILL.md +12 -1
  156. package/dist/agent-src/skills/ai-council/SKILL.md +11 -0
  157. package/dist/agent-src/skills/bug-analyzer/SKILL.md +24 -2
  158. package/dist/agent-src/skills/code-review/SKILL.md +105 -93
  159. package/dist/agent-src/skills/code-review/checklists/backend.md +50 -0
  160. package/dist/agent-src/skills/code-review/checklists/database.md +19 -0
  161. package/dist/agent-src/skills/code-review/checklists/dependency.md +18 -0
  162. package/dist/agent-src/skills/code-review/checklists/docs.md +14 -0
  163. package/dist/agent-src/skills/code-review/checklists/frontend.md +19 -0
  164. package/dist/agent-src/skills/code-review/checklists/infra.md +17 -0
  165. package/dist/agent-src/skills/code-review/checklists/producing-the-review.md +62 -0
  166. package/dist/agent-src/skills/code-review/checklists/test-only.md +13 -0
  167. package/dist/agent-src/skills/code-review/evals/evals.json +53 -15
  168. package/dist/agent-src/skills/condense-memory/SKILL.md +3 -2
  169. package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +15 -0
  170. package/dist/agent-src/skills/docker/SKILL.md +10 -0
  171. package/dist/agent-src/skills/docx-authoring/SKILL.md +102 -0
  172. package/dist/agent-src/skills/github-ci/evals/evals.json +25 -0
  173. package/dist/agent-src/skills/humanizer/SKILL.md +79 -3
  174. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +112 -0
  175. package/dist/agent-src/skills/humanizer/references/fixtures.md +70 -0
  176. package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +7 -1
  177. package/dist/agent-src/skills/laravel/SKILL.md +10 -0
  178. package/dist/agent-src/skills/launch-readiness/SKILL.md +35 -0
  179. package/dist/agent-src/skills/merge-conflicts/SKILL.md +30 -0
  180. package/dist/agent-src/skills/pdf-tools/SKILL.md +101 -0
  181. package/dist/agent-src/skills/quality-tools/evals/evals.json +15 -0
  182. package/dist/agent-src/skills/security-audit/SKILL.md +29 -0
  183. package/dist/agent-src/skills/security-maturity-assessment/SKILL.md +90 -0
  184. package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +52 -0
  185. package/dist/agent-src/skills/skill-writing/SKILL.md +79 -0
  186. package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +11 -3
  187. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +103 -0
  188. package/dist/agent-src/skills/terraform/SKILL.md +10 -0
  189. package/dist/agent-src/skills/test-driven-development/SKILL.md +31 -0
  190. package/dist/agent-src/skills/threat-modeling/SKILL.md +15 -0
  191. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  192. package/dist/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  193. package/dist/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
  194. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
  195. package/dist/agent-src/templates/scripts/telemetry_record.ts +5 -0
  196. package/dist/discovery/deprecation-report.md +1 -1
  197. package/dist/discovery/discovery-manifest.json +256 -190
  198. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  199. package/dist/discovery/discovery-manifest.summary.md +5 -5
  200. package/dist/discovery/orphan-report.md +1 -1
  201. package/dist/discovery/packs.json +20 -17
  202. package/dist/discovery/trust-report.md +3 -3
  203. package/dist/discovery/workspaces.json +16 -13
  204. package/dist/install/install.mjs +10 -2
  205. package/dist/mcp/registry-manifest.json +3 -3
  206. package/dist/router.json +1 -1
  207. package/dist/scripts/condense.js +5 -0
  208. package/dist/scripts/condense.js.map +1 -1
  209. package/dist/server/schemas/settings.js +5 -1
  210. package/dist/server/schemas/settings.js.map +1 -1
  211. package/docs/CLAIMS.md +8 -1
  212. package/docs/SKILL_CENSUS.md +8 -0
  213. package/docs/architecture.md +2 -2
  214. package/docs/benchmark.md +67 -2
  215. package/docs/catalog.md +2 -2
  216. package/docs/contracts/ai-council-config.md +9 -2
  217. package/docs/contracts/ai-team-config.md +37 -4
  218. package/docs/contracts/prelaunch-diagnostics.md +151 -0
  219. package/docs/contracts/release-pr-gating.md +1 -0
  220. package/docs/decisions/ADR-049-configuration-trust-boundary.md +9 -1
  221. package/docs/featured-skills.md +2 -2
  222. package/docs/getting-started-by-role.md +1 -1
  223. package/docs/getting-started.md +16 -5
  224. package/docs/governance-advantage.md +1 -1
  225. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +45 -0
  226. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +28 -0
  227. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +76 -0
  228. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +90 -0
  229. package/docs/guidelines/agent-infra/domain-adoption-gates.md +112 -0
  230. package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
  231. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +99 -0
  232. package/docs/guidelines/agent-infra/skill-quality-checklist.md +13 -0
  233. package/docs/guidelines/design-fidelity-mechanics.md +76 -0
  234. package/docs/parity/command-frontmatter.md +60 -0
  235. package/docs/proof.md +6 -5
  236. package/package.json +1 -1
  237. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  238. package/src/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
  239. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
  240. package/src/agent-src/templates/scripts/telemetry_record.ts +5 -0
  241. package/src/config/agent-settings.template.yml +34 -10
  242. package/src/config/prelaunch-areas.yml +36 -0
  243. package/src/scripts/_cli/cmd_doctor.ts +118 -26
  244. package/src/scripts/_lib/claude_plugin.ts +77 -2
  245. package/src/scripts/_lib/orchestration_record.ts +5 -0
  246. package/src/scripts/_lib/tool_adapter_registry.ts +120 -0
  247. package/src/scripts/ai_team/config.ts +73 -1
  248. package/src/scripts/ai_team/review_gate.ts +636 -0
  249. package/src/scripts/ai_team/team_dispatch.ts +612 -0
  250. package/src/scripts/bench_ab_task_runner.ts +11 -1
  251. package/src/scripts/bench_parity_count.ts +279 -0
  252. package/src/scripts/build_linear_digest.ts +3 -1
  253. package/src/scripts/check_bridge_derivation.ts +172 -0
  254. package/src/scripts/check_consumer_scope_flip.ts +179 -0
  255. package/src/scripts/check_council_config_location.ts +115 -15
  256. package/src/scripts/check_host_loadability.ts +81 -0
  257. package/src/scripts/condense.ts +5 -0
  258. package/src/scripts/hook_manifest.yaml +12 -1
  259. package/src/scripts/lint_handoffs.ts +42 -0
  260. package/src/scripts/lint_skill_descriptions.ts +228 -0
  261. package/src/scripts/lint_skill_descriptions_allowlist.json +1 -0
  262. package/src/scripts/lint_skill_scripts_readonly.ts +145 -0
  263. package/src/scripts/lint_skill_scripts_readonly_allowlist.json +8 -0
  264. package/src/scripts/lint_supported_tools_matrix.ts +35 -0
  265. package/src/scripts/optimize_skill_description.ts +144 -0
  266. package/src/scripts/orchestration_record.ts +2 -1
  267. package/src/scripts/prelaunch_diagnostics.ts +248 -0
  268. package/src/scripts/run_skill_evals.ts +70 -4
  269. package/src/scripts/schemas/command.schema.json +7 -0
  270. package/src/scripts/schemas/evals.schema.json +105 -14
  271. package/src/scripts/schemas/prelaunch-report.schema.json +107 -0
  272. package/src/scripts/schemas/skill.schema.json +18 -1
  273. package/src/scripts/schemas/subagent.schema.json +6 -0
  274. package/src/scripts/skill_linter.ts +43 -3
  275. package/src/scripts/smoke_host_loadability.sh +73 -0
  276. package/src/scripts/team_review_gate_hook.ts +115 -0
  277. package/src/scripts/trigger_eval_grandfather.json +1 -2
  278. package/src/scripts/utilization_report.ts +225 -0
  279. 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 CLI contract is pinned — `--quiet` flag, exit codes (0 clean,
7
- * 1 at least one violation), byte-identical finding messages, same scan globs
8
- * (sorted), same fence tracking, same negation / `ai_council:` block detection,
9
- * and the same `<!-- council-config-allowed -->` escape pragma. No behaviour
10
- * changes — historical quirks preserved (consumers pin the exact behaviour).
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, in the council command/skill surfaces + the config contract:
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(root: string): Generator<string> {
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 SCAN_GLOBS) {
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
- '(docs/contracts/ai-council-config.md + ADR-093), never in ' +
234
- '`.agent-settings.yml`.\n',
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
+ }
@@ -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": [] }