@event4u/agent-config 1.13.0 → 1.15.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 (291) hide show
  1. package/.agent-src/commands/agent-handoff.md +4 -1
  2. package/.agent-src/commands/agent-status.md +3 -0
  3. package/.agent-src/commands/agents-audit.md +4 -0
  4. package/.agent-src/commands/agents-cleanup.md +6 -1
  5. package/.agent-src/commands/agents-prepare.md +3 -0
  6. package/.agent-src/commands/analyze-reference-repo.md +4 -0
  7. package/.agent-src/commands/bug-fix.md +7 -3
  8. package/.agent-src/commands/bug-investigate.md +4 -0
  9. package/.agent-src/commands/chat-history-checkpoint.md +126 -0
  10. package/.agent-src/commands/chat-history-clear.md +6 -1
  11. package/.agent-src/commands/chat-history-resume.md +7 -2
  12. package/.agent-src/commands/chat-history.md +7 -2
  13. package/.agent-src/commands/check-current-md.md +137 -0
  14. package/.agent-src/commands/commit-in-chunks.md +118 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +37 -2
  17. package/.agent-src/commands/context-create.md +4 -0
  18. package/.agent-src/commands/context-refactor.md +4 -0
  19. package/.agent-src/commands/copilot-agents-init.md +3 -0
  20. package/.agent-src/commands/copilot-agents-optimize.md +3 -0
  21. package/.agent-src/commands/create-pr-description.md +4 -0
  22. package/.agent-src/commands/create-pr.md +4 -0
  23. package/.agent-src/commands/do-and-judge.md +4 -1
  24. package/.agent-src/commands/do-in-steps.md +3 -0
  25. package/.agent-src/commands/e2e-heal.md +4 -0
  26. package/.agent-src/commands/e2e-plan.md +4 -0
  27. package/.agent-src/commands/estimate-ticket.md +4 -1
  28. package/.agent-src/commands/feature-dev.md +4 -0
  29. package/.agent-src/commands/feature-explore.md +4 -0
  30. package/.agent-src/commands/feature-plan.md +4 -0
  31. package/.agent-src/commands/feature-refactor.md +4 -0
  32. package/.agent-src/commands/feature-roadmap.md +6 -0
  33. package/.agent-src/commands/fix-ci.md +4 -0
  34. package/.agent-src/commands/fix-portability.md +5 -2
  35. package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
  36. package/.agent-src/commands/fix-pr-comments.md +4 -0
  37. package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
  38. package/.agent-src/commands/fix-references.md +3 -0
  39. package/.agent-src/commands/fix-seeder.md +4 -0
  40. package/.agent-src/commands/implement-ticket.md +39 -13
  41. package/.agent-src/commands/jira-ticket.md +4 -0
  42. package/.agent-src/commands/judge.md +3 -0
  43. package/.agent-src/commands/memory-add.md +5 -3
  44. package/.agent-src/commands/memory-full.md +5 -2
  45. package/.agent-src/commands/memory-promote.md +7 -6
  46. package/.agent-src/commands/mode.md +3 -0
  47. package/.agent-src/commands/module-create.md +4 -0
  48. package/.agent-src/commands/module-explore.md +4 -0
  49. package/.agent-src/commands/onboard.md +33 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +12 -0
  52. package/.agent-src/commands/optimize-rtk-filters.md +3 -0
  53. package/.agent-src/commands/optimize-skills.md +4 -0
  54. package/.agent-src/commands/override-create.md +4 -0
  55. package/.agent-src/commands/override-manage.md +4 -0
  56. package/.agent-src/commands/package-reset.md +3 -0
  57. package/.agent-src/commands/package-test.md +3 -0
  58. package/.agent-src/commands/prepare-for-review.md +4 -0
  59. package/.agent-src/commands/project-analyze.md +4 -0
  60. package/.agent-src/commands/project-health.md +4 -0
  61. package/.agent-src/commands/propose-memory.md +6 -8
  62. package/.agent-src/commands/quality-fix.md +4 -0
  63. package/.agent-src/commands/refine-ticket.md +12 -7
  64. package/.agent-src/commands/review-changes.md +39 -8
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +18 -0
  67. package/.agent-src/commands/roadmap-execute.md +14 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +11 -0
  70. package/.agent-src/commands/sync-agent-settings.md +12 -0
  71. package/.agent-src/commands/sync-gitignore.md +3 -0
  72. package/.agent-src/commands/tests-create.md +4 -0
  73. package/.agent-src/commands/tests-execute.md +6 -3
  74. package/.agent-src/commands/threat-model.md +4 -0
  75. package/.agent-src/commands/update-form-request-messages.md +4 -0
  76. package/.agent-src/commands/upstream-contribute.md +4 -0
  77. package/.agent-src/commands/work.md +161 -0
  78. package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
  79. package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
  80. package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
  81. package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
  82. package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
  83. package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
  84. package/.agent-src/personas/README.md +0 -1
  85. package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
  86. package/.agent-src/rules/artifact-engagement-recording.md +133 -0
  87. package/.agent-src/rules/ask-when-uncertain.md +18 -13
  88. package/.agent-src/rules/augment-portability.md +64 -37
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history-cadence.md +109 -0
  91. package/.agent-src/rules/chat-history-ownership.md +123 -0
  92. package/.agent-src/rules/chat-history-visibility.md +96 -0
  93. package/.agent-src/rules/cli-output-handling.md +27 -4
  94. package/.agent-src/rules/command-suggestion.md +134 -0
  95. package/.agent-src/rules/commit-policy.md +109 -0
  96. package/.agent-src/rules/direct-answers.md +114 -0
  97. package/.agent-src/rules/docs-sync.md +36 -0
  98. package/.agent-src/rules/downstream-changes.md +10 -9
  99. package/.agent-src/rules/improve-before-implement.md +9 -6
  100. package/.agent-src/rules/language-and-tone.md +85 -6
  101. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  102. package/.agent-src/rules/package-ci-checks.md +4 -0
  103. package/.agent-src/rules/preservation-guard.md +20 -0
  104. package/.agent-src/rules/roadmap-progress-sync.md +159 -27
  105. package/.agent-src/rules/role-mode-adherence.md +1 -1
  106. package/.agent-src/rules/scope-control.md +42 -1
  107. package/.agent-src/rules/size-enforcement.md +2 -3
  108. package/.agent-src/rules/skill-quality.md +3 -8
  109. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  110. package/.agent-src/rules/user-interaction.md +107 -51
  111. package/.agent-src/scripts/update_roadmap_progress.py +73 -9
  112. package/.agent-src/skills/blade-ui/SKILL.md +47 -3
  113. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  114. package/.agent-src/skills/command-writing/SKILL.md +52 -2
  115. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  116. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  117. package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
  118. package/.agent-src/skills/fe-design/SKILL.md +78 -61
  119. package/.agent-src/skills/file-editor/SKILL.md +9 -0
  120. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  121. package/.agent-src/skills/flux/SKILL.md +31 -4
  122. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  123. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  124. package/.agent-src/skills/livewire/SKILL.md +49 -4
  125. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  126. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  127. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  128. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  129. package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
  130. package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
  131. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  132. package/.agent-src/skills/skill-writing/SKILL.md +3 -5
  133. package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
  134. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  135. package/.agent-src/templates/AGENTS.md +24 -6
  136. package/.agent-src/templates/agent-settings.md +149 -0
  137. package/.agent-src/templates/roadmaps.md +11 -4
  138. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  139. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  140. package/.agent-src/templates/scripts/memory_lookup.py +1 -1
  141. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  142. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  143. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  144. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  145. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  146. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  147. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  148. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  149. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  150. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  151. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  152. package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
  153. package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
  154. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
  155. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  156. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
  159. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
  160. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
  161. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  162. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
  163. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  164. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  165. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  176. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  177. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  178. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  179. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  180. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  181. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  182. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  183. package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
  184. package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
  185. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  194. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  195. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  196. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  197. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  198. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  199. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  200. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  201. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  202. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  203. package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
  204. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  205. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  206. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  207. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
  208. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
  209. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  210. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  211. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  212. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  213. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  214. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  215. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  216. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  217. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  218. package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
  219. package/.claude-plugin/marketplace.json +105 -2
  220. package/AGENTS.md +38 -8
  221. package/CHANGELOG.md +609 -0
  222. package/README.md +136 -14
  223. package/config/agent-settings.template.yml +45 -0
  224. package/config/gitignore-block.txt +4 -0
  225. package/docs/MIGRATION.md +122 -0
  226. package/docs/architecture.md +111 -35
  227. package/docs/contracts/STABILITY.md +95 -0
  228. package/docs/contracts/adr-chat-history-split.md +132 -0
  229. package/docs/contracts/adr-command-suggestion.md +146 -0
  230. package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
  231. package/docs/contracts/adr-product-ui-track.md +384 -0
  232. package/docs/contracts/adr-prompt-driven-execution.md +187 -0
  233. package/docs/contracts/agent-memory-contract.md +149 -0
  234. package/docs/contracts/artifact-engagement-flow.md +262 -0
  235. package/docs/contracts/command-clusters.md +126 -0
  236. package/docs/contracts/command-suggestion-flow.md +148 -0
  237. package/docs/contracts/implement-ticket-flow.md +628 -0
  238. package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
  239. package/docs/contracts/linear-ai-three-layers.md +131 -0
  240. package/docs/contracts/rule-interactions.md +107 -0
  241. package/docs/contracts/rule-interactions.yml +142 -0
  242. package/docs/contracts/ui-stack-extension.md +236 -0
  243. package/docs/contracts/ui-track-flow.md +338 -0
  244. package/docs/development.md +1 -1
  245. package/docs/getting-started.md +3 -3
  246. package/docs/installation.md +124 -2
  247. package/docs/migrations/commands-1.15.0.md +112 -0
  248. package/docs/showcase.md +204 -0
  249. package/docs/ui-track-mental-model.md +121 -0
  250. package/package.json +1 -1
  251. package/scripts/agent-config +199 -0
  252. package/scripts/audit_cloud_compatibility.py +288 -0
  253. package/scripts/build_cloud_bundle.py +458 -0
  254. package/scripts/build_linear_digest.py +263 -0
  255. package/scripts/chat_history.py +796 -7
  256. package/scripts/check_compression.py +139 -0
  257. package/scripts/check_iron_law_prominence.py +143 -0
  258. package/scripts/check_md_language.py +159 -0
  259. package/scripts/check_portability.py +38 -0
  260. package/scripts/check_public_links.py +185 -0
  261. package/scripts/check_references.py +1 -0
  262. package/scripts/check_reply_consistency.py +140 -0
  263. package/scripts/command_suggester/__init__.py +51 -0
  264. package/scripts/command_suggester/cooldown.py +132 -0
  265. package/scripts/command_suggester/loader.py +70 -0
  266. package/scripts/command_suggester/match.py +180 -0
  267. package/scripts/command_suggester/rank.py +120 -0
  268. package/scripts/command_suggester/render.py +86 -0
  269. package/scripts/command_suggester/sanitize.py +113 -0
  270. package/scripts/command_suggester/settings.py +125 -0
  271. package/scripts/command_suggester/types.py +78 -0
  272. package/scripts/hooks/augment-chat-history.sh +56 -0
  273. package/scripts/install-hooks.sh +67 -0
  274. package/scripts/install.py +150 -33
  275. package/scripts/lint_marketplace.py +27 -0
  276. package/scripts/lint_no_new_atomic_commands.py +179 -0
  277. package/scripts/lint_rule_interactions.py +149 -0
  278. package/scripts/memory_lookup.py +1 -1
  279. package/scripts/migrate_command_suggestions.py +151 -0
  280. package/scripts/release.py +297 -64
  281. package/scripts/schemas/command.schema.json +41 -0
  282. package/scripts/skill_linter.py +81 -0
  283. package/scripts/sync_agent_settings.py +42 -12
  284. package/scripts/update_counts.py +10 -0
  285. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  286. package/templates/consumer-settings/claude-settings.json +55 -1
  287. package/.agent-src/rules/chat-history.md +0 -171
  288. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  289. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  290. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  291. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
@@ -0,0 +1,78 @@
1
+ """Type definitions for the command suggestion engine.
2
+
3
+ Plain dataclasses — no third-party deps. Kept in a sibling module so
4
+ match/rank/cooldown/render can import without cycles.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class CommandSpec:
14
+ """Loaded command metadata that drives matching.
15
+
16
+ Fields mirror the `suggestion:` frontmatter block plus the
17
+ command's `name` and `description`. Ineligible commands are
18
+ represented with `eligible=False` and are never returned by the
19
+ matcher; the loader keeps them so cross-referencing stays simple.
20
+ """
21
+
22
+ name: str
23
+ description: str
24
+ eligible: bool
25
+ trigger_description: str = ""
26
+ trigger_context: str = ""
27
+ rationale: str = ""
28
+ confidence_floor: Optional[float] = None
29
+ cooldown: Optional[str] = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Match:
34
+ """A scored candidate. `score` is 0.0–1.0 inclusive.
35
+
36
+ `matched_trigger` is "description" | "context" | "both" and lets
37
+ the renderer surface why a command surfaced. `evidence` is the
38
+ short substring that fired (debugging / golden tests / explain).
39
+ `has_structural_bonus` is True when a heavy-signal pattern (ticket
40
+ key, file path, glob) co-occurred in the message — the ranker
41
+ treats those as specific enough to bypass vague-input suppression.
42
+ """
43
+
44
+ command: str
45
+ score: float
46
+ matched_trigger: str
47
+ evidence: str
48
+ has_structural_bonus: bool = False
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Settings:
53
+ """Runtime knobs read from `.agent-settings.yml`.
54
+
55
+ Defaults match the "open decisions" leans in the roadmap.
56
+ Per-command frontmatter values override the global floor /
57
+ cooldown.
58
+ """
59
+
60
+ enabled: bool = True
61
+ confidence_floor: float = 0.6
62
+ cooldown_seconds: int = 600 # 10m
63
+ max_options: int = 4
64
+ blocklist: tuple[str, ...] = ()
65
+
66
+
67
+ @dataclass
68
+ class CooldownState:
69
+ """Per-conversation cooldown tracker — mutable on purpose."""
70
+
71
+ last_shown: dict[tuple[str, str], float] = field(default_factory=dict)
72
+ """Key: (command_name, trigger_evidence). Value: unix timestamp."""
73
+
74
+ explicit_invocations: dict[str, float] = field(default_factory=dict)
75
+ """Commands the user explicitly typed; clears their cooldown."""
76
+
77
+ disabled_for_conversation: bool = False
78
+ """Set by the `/command-suggestion-off` directive (Phase 5)."""
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Augment Code lifecycle-hook trampoline for chat-history.
3
+ #
4
+ # Augment requires hook scripts to use the .sh extension and live at
5
+ # either a system path (/etc/augment/...) or user scope
6
+ # (~/.augment/...). This trampoline lives at user scope and dispatches
7
+ # every event to whichever workspace fired it, so a single install
8
+ # covers every project that has ./agent-config available.
9
+ #
10
+ # Behaviour:
11
+ # - Read the JSON event from stdin into a buffer.
12
+ # - Extract workspace_roots[0]; bail silently when missing.
13
+ # - cd into that workspace; bail silently when it is not a directory
14
+ # or does not contain ./agent-config.
15
+ # - Re-pipe the original JSON into
16
+ # ./agent-config chat-history:hook --platform augment
17
+ # so chat_history.py can run the platform mapping.
18
+ # - Always exit 0 — chat-history must never block the agent loop.
19
+
20
+ set -u
21
+
22
+ EVENT_DATA="$(cat)"
23
+
24
+ # Extract workspace_roots[0] using whichever JSON tool is available.
25
+ WORKSPACE=""
26
+ if command -v jq >/dev/null 2>&1; then
27
+ WORKSPACE="$(printf '%s' "$EVENT_DATA" \
28
+ | jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
29
+ elif command -v python3 >/dev/null 2>&1; then
30
+ WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
31
+ import json, sys
32
+ try:
33
+ data = json.load(sys.stdin)
34
+ except Exception:
35
+ sys.exit(0)
36
+ roots = data.get("workspace_roots") or []
37
+ if roots:
38
+ print(roots[0])
39
+ ' 2>/dev/null)"
40
+ fi
41
+
42
+ if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ cd "$WORKSPACE" 2>/dev/null || exit 0
47
+
48
+ if [ ! -x ./agent-config ]; then
49
+ exit 0
50
+ fi
51
+
52
+ printf '%s' "$EVENT_DATA" \
53
+ | ./agent-config chat-history:hook --platform augment \
54
+ >/dev/null 2>&1 || true
55
+
56
+ exit 0
@@ -27,3 +27,70 @@ EOF
27
27
 
28
28
  chmod +x "$HOOKS_DIR/pre-push"
29
29
  echo "✅ Pre-push hook installed."
30
+
31
+ # Pre-commit: marketplace consistency -----------------------------------------
32
+ #
33
+ # Distribution manifests (.claude-plugin/marketplace.json) drift silently —
34
+ # adding a skill on disk without updating the manifest renders it invisible to
35
+ # Claude Code Plugin Marketplace consumers. CI catches it, but a structural
36
+ # pre-commit gate stops the bad commit from landing in the first place.
37
+ # Runtime is ~40 ms; always-on is cheaper than scoped detection.
38
+
39
+ cat > "$HOOKS_DIR/pre-commit" << 'EOF'
40
+ #!/usr/bin/env bash
41
+ # Pre-commit hook: verify .claude-plugin/marketplace.json lists every skill
42
+ # that exists on disk under .claude/skills/.
43
+
44
+ python3 scripts/lint_marketplace.py
45
+ status=$?
46
+
47
+ if [ $status -ne 0 ]; then
48
+ echo ""
49
+ echo "❌ Commit blocked — .claude-plugin/marketplace.json is out of sync."
50
+ echo " Add the missing skill to the manifest (or remove the stale entry),"
51
+ echo " then re-stage and commit. To bypass for an unrelated WIP commit:"
52
+ echo " git commit --no-verify"
53
+ exit 1
54
+ fi
55
+ EOF
56
+
57
+ chmod +x "$HOOKS_DIR/pre-commit"
58
+ echo "✅ Pre-commit hook installed."
59
+
60
+ # Chat-history bridge hooks ----------------------------------------------------
61
+ #
62
+ # Augment IDE plugin (and any other agent surface without native chat
63
+ # lifecycle hooks) cannot fire SessionStart/Stop/PostToolUse. Git hooks
64
+ # are the platform-agnostic lifecycle surface that fires regardless of
65
+ # IDE — every commit, merge, checkout, and rewrite turns into a phase
66
+ # boundary in .agent-chat-history when an agent session is active.
67
+ #
68
+ # The hooks are silent no-ops when no agent session is active (the
69
+ # chat_history.py hook-append script returns "skipped_no_sidecar" with
70
+ # exit 0) and `|| true` belt-and-suspenders ensures git operations are
71
+ # never blocked.
72
+
73
+ write_chat_history_hook() {
74
+ local name="$1"
75
+ local phase_tag="$2"
76
+ cat > "$HOOKS_DIR/$name" << EOF
77
+ #!/usr/bin/env bash
78
+ # $name: append a phase boundary to .agent-chat-history when an agent
79
+ # session is active. Silent no-op otherwise. Never blocks git.
80
+
81
+ if [ -x ./agent-config ]; then
82
+ ref="\$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
83
+ payload="{\"phase\":\"$phase_tag\",\"source\":\"git-hook:\$ref\"}"
84
+ ./agent-config chat-history:checkpoint --payload "\$payload" \
85
+ >/dev/null 2>&1 || true
86
+ fi
87
+ exit 0
88
+ EOF
89
+ chmod +x "$HOOKS_DIR/$name"
90
+ echo "✅ $name hook installed."
91
+ }
92
+
93
+ write_chat_history_hook "post-commit" "git:post-commit"
94
+ write_chat_history_hook "post-merge" "git:post-merge"
95
+ write_chat_history_hook "post-checkout" "git:post-checkout"
96
+ write_chat_history_hook "post-rewrite" "git:post-rewrite"
@@ -251,45 +251,67 @@ def _yaml_scalar(value: str) -> str:
251
251
  def _replace_template_value(template: str, dotted_path: str, value: str) -> str:
252
252
  """Replace the default value for a dotted-path key in the YAML template.
253
253
 
254
- Strategy: walk the template lines, track the current top-level
255
- section, and replace the first matching line. Comments and indentation
256
- are preserved.
254
+ Convenience wrapper: formats *value* as a YAML scalar (via
255
+ :func:`_yaml_scalar`) and delegates to :func:`_replace_template_value_raw`.
256
+ """
257
+ return _replace_template_value_raw(template, dotted_path, _yaml_scalar(value))
258
+
259
+
260
+ def _replace_template_value_raw(template: str, dotted_path: str, raw_yaml: str) -> str:
261
+ """Replace the value at *dotted_path* with the pre-formatted *raw_yaml*.
262
+
263
+ Handles arbitrary nesting depth. The template uses 2-space indents;
264
+ parent sections are tracked by indent level so the leaf scalar is
265
+ only replaced when every parent matches the dotted path.
266
+
267
+ Comments and indentation are preserved. Returns *template* unchanged
268
+ if the path cannot be located.
257
269
  """
258
270
  parts = dotted_path.split(".")
259
- if len(parts) == 1:
260
- section, key = None, parts[0]
261
- elif len(parts) == 2:
262
- section, key = parts[0], parts[1]
263
- else:
264
- return template # deeper nesting not supported in current schema
271
+ if not parts:
272
+ return template
265
273
 
266
- lines = template.splitlines()
267
- current_section: "str | None" = None
268
- section_re = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$")
269
- scalar_top_re = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*.*$")
270
- scalar_sub_re = re.compile(r"^(\s+)([A-Za-z_][A-Za-z0-9_]*):\s*.*$")
274
+ sections = parts[:-1]
275
+ key = parts[-1]
276
+ target_indent = " " * len(sections)
277
+
278
+ header_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*$")
279
+ scalar_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*\S.*$")
280
+
281
+ # Stack of section names by depth; None entries mean "not yet seen
282
+ # at this depth" or "left this section". For path a.b.c we need
283
+ # current_path == ['a', 'b'] when scanning for key 'c' at indent 4.
284
+ current_path: list[str | None] = [None] * len(sections)
271
285
 
272
- replacement = _yaml_scalar(value)
286
+ lines = template.splitlines()
273
287
  for idx, line in enumerate(lines):
274
- # Top-level section header
275
- m_section = section_re.match(line)
276
- if m_section:
277
- current_section = m_section.group(1)
288
+ stripped = line.strip()
289
+ if not stripped or stripped.startswith("#"):
278
290
  continue
279
- if section is None:
280
- # Top-level scalar target
281
- m_top = scalar_top_re.match(line)
282
- if m_top and m_top.group(1) == key and not line.startswith((" ", "\t")):
283
- lines[idx] = f"{key}: {replacement}"
284
- return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
285
- else:
286
- if current_section != section:
287
- continue
288
- m_sub = scalar_sub_re.match(line)
289
- if m_sub and m_sub.group(2) == key:
290
- indent = m_sub.group(1)
291
- lines[idx] = f"{indent}{key}: {replacement}"
292
- return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
291
+
292
+ m_header = header_re.match(line)
293
+ if m_header:
294
+ indent = m_header.group(1)
295
+ name = m_header.group(2)
296
+ depth = len(indent) // 2
297
+ if depth < len(sections):
298
+ current_path[depth] = name
299
+ # Reset deeper levels — we just entered a new sub-tree.
300
+ for d in range(depth + 1, len(sections)):
301
+ current_path[d] = None
302
+ continue
303
+
304
+ m_scalar = scalar_re.match(line)
305
+ if not m_scalar:
306
+ continue
307
+ indent = m_scalar.group(1)
308
+ name = m_scalar.group(2)
309
+ if name != key or indent != target_indent:
310
+ continue
311
+ if current_path != list(sections):
312
+ continue
313
+ lines[idx] = f"{indent}{key}: {raw_yaml}"
314
+ return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
293
315
  return template
294
316
 
295
317
 
@@ -436,6 +458,92 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
436
458
  merge_json_file(project_root / ".augment" / "settings.json", bridge, force, ".augment/settings.json")
437
459
 
438
460
 
461
+ # Augment lifecycle hooks live at user scope (~/.augment/settings.json) per
462
+ # https://docs.augmentcode.com/cli/hooks — that is the only path read by both
463
+ # the CLI and the IDE plugins (VSCode, IntelliJ). Project-local
464
+ # .augment/settings.json is plugin enablement, not hooks.
465
+ AUGMENT_USER_DIR = Path.home() / ".augment"
466
+ AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
467
+ AUGMENT_TRAMPOLINE_NAME = "augment-chat-history.sh"
468
+ AUGMENT_HOOK_EVENTS = ("SessionStart", "SessionEnd", "Stop", "PostToolUse")
469
+
470
+
471
+ def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
472
+ """Deploy the Augment lifecycle-hook trampoline at user scope.
473
+
474
+ Augment hook scripts must use the .sh extension and be referenced by
475
+ absolute path; user scope is the only surface that fires for both the
476
+ CLI and the IDE plugins. This installs once per developer (not per
477
+ project) — the trampoline reads workspace_roots from the event payload
478
+ and dispatches into whichever project is active at hook-fire time.
479
+ """
480
+ src = package_root / "scripts" / "hooks" / AUGMENT_TRAMPOLINE_NAME
481
+ if not src.exists():
482
+ skip(f"augment trampoline missing in package: {src}")
483
+ return
484
+
485
+ AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
486
+ dst = AUGMENT_USER_HOOKS_DIR / AUGMENT_TRAMPOLINE_NAME
487
+
488
+ src_text = src.read_text(encoding="utf-8")
489
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
490
+ skip(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} already up to date")
491
+ else:
492
+ dst.write_text(src_text, encoding="utf-8")
493
+ dst.chmod(0o755)
494
+ success(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} installed")
495
+
496
+ hook_entry = {
497
+ "hooks": [
498
+ {
499
+ "type": "command",
500
+ "command": str(dst),
501
+ },
502
+ ],
503
+ }
504
+ settings_patch: dict = {"hooks": {event: [hook_entry] for event in AUGMENT_HOOK_EVENTS}}
505
+ merge_json_file(
506
+ AUGMENT_USER_DIR / "settings.json",
507
+ settings_patch,
508
+ force,
509
+ "~/.augment/settings.json",
510
+ )
511
+
512
+
513
+ def _chat_history_hook_block(platform: str) -> dict:
514
+ """Single hook entry that calls ./agent-config chat-history:hook --platform <name>."""
515
+ return {
516
+ "hooks": [
517
+ {
518
+ "type": "command",
519
+ "command": f"./agent-config chat-history:hook --platform {platform}",
520
+ },
521
+ ],
522
+ }
523
+
524
+
525
+ def ensure_claude_bridge(project_root: Path, force: bool) -> None:
526
+ """Deploy .claude/settings.json with plugin enablement and chat-history hooks.
527
+
528
+ Hooks dispatch to scripts/chat_history.py via the project-root ./agent-config
529
+ wrapper. They are no-ops when chat_history.enabled is false in
530
+ .agent-settings.yml. Idempotent: reruns merge cleanly without duplicating
531
+ entries (deep_merge replaces hook arrays rather than appending).
532
+ """
533
+ claude_hook = _chat_history_hook_block("claude")
534
+ bridge = {
535
+ "enabledPlugins": {"agent-conf@event4u": True},
536
+ "hooks": {
537
+ "SessionStart": [claude_hook],
538
+ "UserPromptSubmit": [claude_hook],
539
+ "PostToolUse": [claude_hook],
540
+ "Stop": [claude_hook],
541
+ "SessionEnd": [claude_hook],
542
+ },
543
+ }
544
+ merge_json_file(project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json")
545
+
546
+
439
547
  def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
440
548
  target = project_root / ".github" / "plugin" / "marketplace.json"
441
549
 
@@ -474,6 +582,11 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
474
582
  )
475
583
  parser.add_argument("--force", action="store_true", help="overwrite existing files")
476
584
  parser.add_argument("--skip-bridges", action="store_true", help="only create .agent-settings.yml")
585
+ parser.add_argument(
586
+ "--augment-user-hooks",
587
+ action="store_true",
588
+ help="also deploy ~/.augment/settings.json + ~/.augment/hooks/ (user-scope, all projects)",
589
+ )
477
590
  parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
478
591
  parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
479
592
  parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
@@ -516,8 +629,12 @@ def main(argv: list[str]) -> int:
516
629
  if not opts.skip_bridges:
517
630
  ensure_vscode_bridge(project_root, package_type, opts.force)
518
631
  ensure_augment_bridge(project_root, opts.force)
632
+ ensure_claude_bridge(project_root, opts.force)
519
633
  ensure_copilot_bridge(project_root, opts.force)
520
634
 
635
+ if opts.augment_user_hooks:
636
+ ensure_augment_user_hooks(package_root, opts.force)
637
+
521
638
  if not QUIET:
522
639
  print()
523
640
  success("Done.")
@@ -10,6 +10,8 @@ shape used by anthropics/skills:
10
10
  - metadata must have description + version
11
11
  - metadata.version must match package.json (single source of truth)
12
12
  - every plugins[].skills[] entry must exist on disk and carry a SKILL.md
13
+ - every SKILL.md on disk under .claude/skills/ must be listed in some
14
+ plugin's skills[] (drift detection)
13
15
 
14
16
  Exit codes: 0 = clean, 1 = problems found, 3 = internal error.
15
17
  """
@@ -23,6 +25,7 @@ from pathlib import Path
23
25
  ROOT = Path(".")
24
26
  MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json"
25
27
  PACKAGE_JSON = ROOT / "package.json"
28
+ CLAUDE_SKILLS_DIR = ROOT / ".claude" / "skills"
26
29
 
27
30
 
28
31
  def fail(errors: list[str]) -> int:
@@ -121,6 +124,30 @@ def main() -> int:
121
124
  if not skill_md.exists():
122
125
  errors.append(f"{entry} has no SKILL.md: `{path}`")
123
126
 
127
+ # Reverse-completeness: every SKILL.md on disk under .claude/skills/
128
+ # must appear in some plugin's skills[]. Catches the drift where new
129
+ # skills are generated but never added to the marketplace manifest.
130
+ listed: set[str] = set()
131
+ for plugin in plugins:
132
+ if not isinstance(plugin, dict):
133
+ continue
134
+ for path in plugin.get("skills", []):
135
+ if isinstance(path, str):
136
+ listed.add(path.removeprefix("./"))
137
+
138
+ if CLAUDE_SKILLS_DIR.exists():
139
+ for skill_dir in sorted(CLAUDE_SKILLS_DIR.iterdir()):
140
+ if not skill_dir.is_dir():
141
+ continue
142
+ if not (skill_dir / "SKILL.md").exists():
143
+ continue
144
+ rel = f".claude/skills/{skill_dir.name}"
145
+ if rel not in listed:
146
+ errors.append(
147
+ f"skill exists on disk but is not listed in marketplace.json: "
148
+ f"`./{rel}`"
149
+ )
150
+
124
151
  if errors:
125
152
  return fail(errors)
126
153
 
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Atomic-command linter for the command-collapse policy.
4
+
5
+ Reads the locked verb clusters from `docs/contracts/command-clusters.md`,
6
+ finds every command file under `.agent-src.uncompressed/commands/` that
7
+ was **added** since `--baseline` (default: `main`), and requires each
8
+ new file to declare either:
9
+
10
+ - `cluster: <locked-name>` (file is a cluster entry or sub-command), or
11
+ - `superseded_by: <slug>` (file is a deprecation shim).
12
+
13
+ Modifications to pre-existing files are NOT flagged — only additions.
14
+ This stops the atomic surface from growing without forcing every existing
15
+ command into a Phase 1 cluster (most aren't in Phase 1).
16
+
17
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
18
+
19
+ Usage:
20
+ python3 scripts/lint_no_new_atomic_commands.py
21
+ python3 scripts/lint_no_new_atomic_commands.py --baseline origin/main
22
+ python3 scripts/lint_no_new_atomic_commands.py --all # ignore baseline
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import re
29
+ import subprocess
30
+ import sys
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+
34
+ ROOT = Path(__file__).resolve().parent.parent
35
+ COMMANDS_DIR = Path(".agent-src.uncompressed/commands")
36
+ CLUSTER_CONTRACT = Path("docs/contracts/command-clusters.md")
37
+
38
+
39
+ @dataclass
40
+ class Violation:
41
+ file: str
42
+ reason: str
43
+
44
+
45
+ def load_locked_clusters() -> set[str]:
46
+ """Parse the Phase 1 cluster table from the locked contract."""
47
+ text = (ROOT / CLUSTER_CONTRACT).read_text(encoding="utf-8")
48
+ # Locate the Phase 1 table; cluster names sit in backticks in column 1.
49
+ in_phase_1 = False
50
+ clusters: set[str] = set()
51
+ for line in text.splitlines():
52
+ if line.startswith("## Phase 1 clusters"):
53
+ in_phase_1 = True
54
+ continue
55
+ if in_phase_1 and line.startswith("## "):
56
+ break
57
+ if in_phase_1:
58
+ m = re.match(r"\|\s*`([a-z][a-z0-9-]*)`\s*\|", line)
59
+ if m:
60
+ clusters.add(m.group(1))
61
+ if not clusters:
62
+ print(
63
+ f"❌ Could not parse Phase 1 cluster table from {CLUSTER_CONTRACT}",
64
+ file=sys.stderr,
65
+ )
66
+ sys.exit(3)
67
+ return clusters
68
+
69
+
70
+ def added_command_files(baseline: str) -> list[Path]:
71
+ """Files under commands/ added (status A) since baseline."""
72
+ try:
73
+ result = subprocess.run(
74
+ ["git", "diff", "--name-only", "--diff-filter=A",
75
+ f"{baseline}...HEAD", "--", str(COMMANDS_DIR)],
76
+ capture_output=True, text=True, cwd=ROOT, timeout=15,
77
+ )
78
+ except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
79
+ print(f"❌ git diff failed: {exc}", file=sys.stderr)
80
+ sys.exit(3)
81
+ if result.returncode != 0:
82
+ print(f"❌ git diff exit {result.returncode}: {result.stderr}",
83
+ file=sys.stderr)
84
+ sys.exit(3)
85
+ files = [Path(p) for p in result.stdout.splitlines()
86
+ if p.endswith(".md") and p != ""]
87
+ # Also include untracked (newly added, uncommitted) files.
88
+ try:
89
+ wt = subprocess.run(
90
+ ["git", "status", "--porcelain", "--", str(COMMANDS_DIR)],
91
+ capture_output=True, text=True, cwd=ROOT, timeout=10,
92
+ )
93
+ for line in wt.stdout.splitlines():
94
+ if len(line) < 4:
95
+ continue
96
+ status = line[:2]
97
+ if status.strip() not in ("A", "??", "AM"):
98
+ continue
99
+ path = line[3:].strip().split(" -> ")[-1]
100
+ if path.endswith(".md"):
101
+ p = Path(path)
102
+ if p not in files:
103
+ files.append(p)
104
+ except (FileNotFoundError, subprocess.TimeoutExpired):
105
+ pass
106
+ return files
107
+
108
+
109
+ def all_command_files() -> list[Path]:
110
+ return sorted((ROOT / COMMANDS_DIR).glob("*.md"))
111
+
112
+
113
+ def parse_frontmatter(path: Path) -> dict[str, str]:
114
+ text = path.read_text(encoding="utf-8")
115
+ if not text.startswith("---"):
116
+ return {}
117
+ end = text.find("\n---", 3)
118
+ if end == -1:
119
+ return {}
120
+ fm: dict[str, str] = {}
121
+ for line in text[3:end].splitlines():
122
+ if ":" in line:
123
+ k, _, v = line.partition(":")
124
+ fm[k.strip()] = v.strip()
125
+ return fm
126
+
127
+
128
+ def check_file(path: Path, clusters: set[str]) -> Violation | None:
129
+ abs_path = path if path.is_absolute() else ROOT / path
130
+ if not abs_path.exists():
131
+ return None # deleted file, nothing to check
132
+ fm = parse_frontmatter(abs_path)
133
+ if "superseded_by" in fm:
134
+ return None # shim — exempt
135
+ cluster = fm.get("cluster")
136
+ if not cluster:
137
+ return Violation(str(path),
138
+ "missing `cluster:` frontmatter "
139
+ f"(allowed: {sorted(clusters)})")
140
+ if cluster not in clusters:
141
+ return Violation(str(path),
142
+ f"`cluster: {cluster}` is not a locked cluster "
143
+ f"(allowed: {sorted(clusters)})")
144
+ return None
145
+
146
+
147
+ def main() -> int:
148
+ ap = argparse.ArgumentParser(description=__doc__)
149
+ ap.add_argument("--baseline", default="main",
150
+ help="git ref to diff against (default: main)")
151
+ ap.add_argument("--all", action="store_true",
152
+ help="check every command file, not just changed ones")
153
+ args = ap.parse_args()
154
+
155
+ clusters = load_locked_clusters()
156
+ targets = (all_command_files() if args.all
157
+ else added_command_files(args.baseline))
158
+ if not targets:
159
+ print(f"✅ No new commands added under {COMMANDS_DIR} "
160
+ f"(baseline: {args.baseline}).")
161
+ return 0
162
+
163
+ violations = [v for v in (check_file(p, clusters) for p in targets)
164
+ if v is not None]
165
+ if violations:
166
+ print(f"❌ {len(violations)} newly-added atomic command(s) violate "
167
+ f"the command-cluster policy:")
168
+ for v in violations:
169
+ print(f" • {v.file} — {v.reason}")
170
+ print(f"\nSee docs/contracts/command-clusters.md for the locked "
171
+ f"cluster names and frontmatter contract.")
172
+ return 1
173
+ print(f"✅ {len(targets)} newly-added command(s) all declare a valid "
174
+ f"`cluster:` (or `superseded_by:`).")
175
+ return 0
176
+
177
+
178
+ if __name__ == "__main__":
179
+ sys.exit(main())