@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,185 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Public-link checker for the agent-config public surface.
4
+
5
+ Scans the public-surface files (README.md, AGENTS.md, docs/architecture.md)
6
+ for markdown links into `docs/contracts/`, then validates each link against
7
+ the `stability:` frontmatter declared by the target file (per
8
+ `docs/contracts/STABILITY.md`).
9
+
10
+ Rules:
11
+ - target stability=stable → OK (no marker required).
12
+ - target stability=beta → OK; warns if surrounding text has no
13
+ visible "(beta)" marker.
14
+ - target stability=experimental → ERROR. Public surface MUST NOT link
15
+ to experimental contracts.
16
+ - target outside docs/contracts/ but referenced for contract-shaped
17
+ intent (links into agents/contexts/*.md from public files) → ERROR.
18
+ - target file missing → ERROR.
19
+ - target file under docs/contracts/ without `stability:` frontmatter
20
+ (except STABILITY.md itself) → ERROR.
21
+
22
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
23
+
24
+ Usage:
25
+ python3 scripts/check_public_links.py
26
+ python3 scripts/check_public_links.py --list # list contracts + levels
27
+ python3 scripts/check_public_links.py --json # machine-readable
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ import re
35
+ import sys
36
+ from dataclasses import dataclass, asdict
37
+ from pathlib import Path
38
+
39
+ ROOT = Path(__file__).resolve().parent.parent
40
+ PUBLIC_FILES = [Path("README.md"), Path("AGENTS.md"), Path("docs/architecture.md")]
41
+ CONTRACTS_DIR = Path("docs/contracts")
42
+ STABILITY_FILE = CONTRACTS_DIR / "STABILITY.md"
43
+
44
+ LINK_RE = re.compile(r"\[(?P<text>[^\]]+)\]\((?P<href>[^)\s]+)(?:\s+\"[^\"]*\")?\)")
45
+ FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
46
+ STABILITY_RE = re.compile(r"^stability:\s*(\w+)\s*$", re.MULTILINE)
47
+
48
+
49
+ @dataclass
50
+ class Violation:
51
+ file: str
52
+ line: int
53
+ href: str
54
+ reason: str
55
+ severity: str # "error" | "warning"
56
+
57
+
58
+ def read_stability(path: Path) -> str | None:
59
+ if not path.exists():
60
+ return None
61
+ txt = path.read_text(encoding="utf-8")
62
+ m = FRONTMATTER_RE.match(txt)
63
+ if not m:
64
+ return None
65
+ sm = STABILITY_RE.search(m.group(1))
66
+ return sm.group(1) if sm else None
67
+
68
+
69
+ def collect_contracts() -> dict[Path, str | None]:
70
+ out: dict[Path, str | None] = {}
71
+ for p in sorted((ROOT / CONTRACTS_DIR).glob("*.md")):
72
+ rel = p.relative_to(ROOT)
73
+ out[rel] = read_stability(p)
74
+ return out
75
+
76
+
77
+ def resolve(public_file: Path, href: str) -> Path | None:
78
+ href = href.split("#", 1)[0]
79
+ if not href or href.startswith(("http://", "https://", "mailto:", "tel:")):
80
+ return None
81
+ if href.startswith("/"):
82
+ return Path(href.lstrip("/"))
83
+ return (public_file.parent / href).resolve().relative_to(ROOT.resolve())
84
+
85
+
86
+ def scan_file(public_file: Path, contracts: dict[Path, str | None]) -> list[Violation]:
87
+ abs_path = ROOT / public_file
88
+ if not abs_path.exists():
89
+ return []
90
+ violations: list[Violation] = []
91
+ for lineno, line in enumerate(abs_path.read_text(encoding="utf-8").splitlines(), 1):
92
+ for m in LINK_RE.finditer(line):
93
+ href = m.group("href")
94
+ text = m.group("text")
95
+ try:
96
+ target = resolve(public_file, href)
97
+ except ValueError:
98
+ continue
99
+ if target is None:
100
+ continue
101
+ if target.parts[:2] == ("agents", "contexts") and target.suffix == ".md":
102
+ violations.append(Violation(str(public_file), lineno, href,
103
+ "public surface MUST NOT link into agents/contexts/ — move target to docs/contracts/",
104
+ "error"))
105
+ continue
106
+ if target.parts[:2] != ("docs", "contracts") or target.suffix != ".md":
107
+ continue
108
+ if target == STABILITY_FILE:
109
+ continue
110
+ if target not in contracts:
111
+ violations.append(Violation(str(public_file), lineno, href,
112
+ f"target not found: {target}", "error"))
113
+ continue
114
+ level = contracts[target]
115
+ if level is None:
116
+ violations.append(Violation(str(public_file), lineno, href,
117
+ f"target missing 'stability:' frontmatter: {target}", "error"))
118
+ continue
119
+ if level == "experimental":
120
+ violations.append(Violation(str(public_file), lineno, href,
121
+ f"public surface MUST NOT link to experimental contract: {target}",
122
+ "error"))
123
+ continue
124
+ if level == "beta":
125
+ window = line.lower()
126
+ if "(beta)" not in window and "[beta]" not in window:
127
+ violations.append(Violation(str(public_file), lineno, href,
128
+ f"link to beta contract '{target}' lacks visible (beta) marker",
129
+ "warning"))
130
+ return violations
131
+
132
+
133
+ def main() -> int:
134
+ ap = argparse.ArgumentParser()
135
+ ap.add_argument("--list", action="store_true", help="list contracts + stability levels")
136
+ ap.add_argument("--json", action="store_true", help="machine-readable output")
137
+ ap.add_argument("--strict", action="store_true",
138
+ help="fail on warnings as well as errors (default: errors only)")
139
+ args = ap.parse_args()
140
+
141
+ contracts = collect_contracts()
142
+ if args.list:
143
+ for p, lvl in contracts.items():
144
+ print(f" {lvl or '(no frontmatter)':14} {p}")
145
+ return 0
146
+
147
+ missing_fm = [p for p, lvl in contracts.items() if lvl is None and p != STABILITY_FILE]
148
+ violations: list[Violation] = []
149
+ for p in missing_fm:
150
+ violations.append(Violation(str(p), 0, "(self)",
151
+ "missing 'stability:' frontmatter required by docs/contracts/STABILITY.md",
152
+ "error"))
153
+ for f in PUBLIC_FILES:
154
+ violations.extend(scan_file(f, contracts))
155
+
156
+ if args.json:
157
+ print(json.dumps([asdict(v) for v in violations], indent=2))
158
+ else:
159
+ errors = [v for v in violations if v.severity == "error"]
160
+ warnings = [v for v in violations if v.severity == "warning"]
161
+ for v in violations:
162
+ icon = "❌" if v.severity == "error" else "⚠️ "
163
+ loc = f"{v.file}:{v.line}" if v.line else v.file
164
+ print(f"{icon} {loc} {v.href}\n → {v.reason}")
165
+ if not violations:
166
+ print(f"✅ public-link check clean — {len(contracts)} contracts scanned, "
167
+ f"{len(PUBLIC_FILES)} public files clean")
168
+ else:
169
+ print(f"\nsummary: {len(errors)} error(s), {len(warnings)} warning(s)")
170
+
171
+ has_errors = any(v.severity == "error" for v in violations)
172
+ has_warnings = any(v.severity == "warning" for v in violations)
173
+ if has_errors:
174
+ return 1
175
+ if has_warnings and args.strict:
176
+ return 1
177
+ return 0
178
+
179
+
180
+ if __name__ == "__main__":
181
+ try:
182
+ sys.exit(main())
183
+ except Exception as e:
184
+ print(f"❌ internal error: {e}", file=sys.stderr)
185
+ sys.exit(3)
@@ -78,6 +78,7 @@ EXAMPLE_PATH_PATTERNS = [
78
78
  re.compile(r"agents/overrides/"), # override examples
79
79
  re.compile(r"commands/old-cmd"), # example placeholder
80
80
  re.compile(r"agents/README"), # README reference (may not exist in package)
81
+ re.compile(r"agents/index[\w.-]*\.md"), # planned auto-generated artefact index (F5)
81
82
  re.compile(r"agents/docs/"), # project-specific docs (not in package)
82
83
  re.compile(r"agents/contexts/"), # project-specific contexts (not in package)
83
84
  re.compile(r"agents/gates"), # project-specific policy docs
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env python3
2
+ """check_reply_consistency.py — enforce user-interaction.md Iron Laws.
3
+
4
+ Single-Source Recommendation Line: a reply with numbered options must
5
+ have ONE bolded `Recommendation: N` / `Empfehlung: N` line, no inline
6
+ `(recommended)` / `(rec)` / `(empfohlen)` tag next to options, and the
7
+ recommended number must appear in the option block.
8
+
9
+ Modes:
10
+ --stdin / --file <path> Validate a single draft (all rules).
11
+ --scan-dir <dir> Scan .md tree for legacy inline-tag regression.
12
+
13
+ Exit codes:
14
+ 0 ok · 2 inline tag · 3 multi-rec · 4 rec-not-in-options
15
+ 5 options-without-rec (strict) · 6 scan-dir found · 9 usage error
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ OPTION_LINE_RE = re.compile(r"^\s*>?\s*(\d+)\.\s+\S")
25
+ REC_LINE_RE = re.compile(
26
+ r"(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b", re.IGNORECASE
27
+ )
28
+ TAG_RE = re.compile(r"\((?:recommended|rec|empfohlen)\)", re.IGNORECASE)
29
+ CODESPAN_RE = re.compile(r"`[^`\n]*`")
30
+
31
+
32
+ def _strip_codespans(line: str) -> str:
33
+ return CODESPAN_RE.sub("``", line)
34
+
35
+
36
+ def find_inline_tag(text: str) -> tuple[int, str] | None:
37
+ """Return (line_no, raw_line) of the first numbered-option line carrying
38
+ an inline (recommended)-class tag outside code spans, or None."""
39
+ for idx, raw in enumerate(text.splitlines(), start=1):
40
+ stripped = _strip_codespans(raw)
41
+ if not OPTION_LINE_RE.match(stripped):
42
+ continue
43
+ if TAG_RE.search(stripped):
44
+ return idx, raw.strip()
45
+ return None
46
+
47
+
48
+ def find_option_blocks(text: str) -> list[list[int]]:
49
+ """Group consecutive numbered-option lines into blocks; return list of
50
+ blocks, each a list of the numbers found in that block."""
51
+ blocks: list[list[int]] = []
52
+ current: list[int] = []
53
+ for raw in text.splitlines():
54
+ m = OPTION_LINE_RE.match(raw)
55
+ if m:
56
+ current.append(int(m.group(1)))
57
+ else:
58
+ if len(current) >= 2:
59
+ blocks.append(current)
60
+ current = []
61
+ if len(current) >= 2:
62
+ blocks.append(current)
63
+ return blocks
64
+
65
+
66
+ def validate(text: str, strict: bool = False) -> tuple[int, str]:
67
+ """Run rules. Returns (exit_code, human_message)."""
68
+ tag = find_inline_tag(text)
69
+ if tag:
70
+ line_no, snippet = tag
71
+ return 2, f"line {line_no}: inline tag on numbered option — {snippet!r}"
72
+
73
+ blocks = find_option_blocks(text)
74
+ rec_numbers = [int(n) for n in REC_LINE_RE.findall(text)]
75
+
76
+ if not blocks:
77
+ return 0, "ok (no numbered options block)"
78
+
79
+ if not rec_numbers:
80
+ if strict:
81
+ return 5, "numbered options without Recommendation:/Empfehlung: line"
82
+ return 0, "ok (options without recommendation; non-strict)"
83
+
84
+ distinct = sorted(set(rec_numbers))
85
+ if len(distinct) > 1:
86
+ return 3, f"multiple distinct recommendation numbers: {distinct}"
87
+
88
+ rec_num = distinct[0]
89
+ for block in blocks:
90
+ if rec_num in block:
91
+ return 0, f"ok (recommendation {rec_num} matches option block)"
92
+ return 4, f"recommendation {rec_num} not present in any option block"
93
+
94
+
95
+ def cmd_scan_dir(root: Path) -> int:
96
+ if not root.is_dir():
97
+ print(f"error: not a directory: {root}", file=sys.stderr)
98
+ return 9
99
+ violations: list[tuple[Path, int, str]] = []
100
+ for md in sorted(root.rglob("*.md")):
101
+ text = md.read_text(encoding="utf-8")
102
+ for idx, raw in enumerate(text.splitlines(), start=1):
103
+ stripped = _strip_codespans(raw)
104
+ if OPTION_LINE_RE.match(stripped) and TAG_RE.search(stripped):
105
+ violations.append((md, idx, raw.strip()))
106
+ if violations:
107
+ for path, line, snippet in violations:
108
+ print(f" 🔴 {path}:{line} — inline-tag — {snippet}", file=sys.stderr)
109
+ print(f"\n❌ {len(violations)} legacy-pattern violation(s)", file=sys.stderr)
110
+ return 6
111
+ print(f"✅ No legacy (recommended) tags found under {root}")
112
+ return 0
113
+
114
+
115
+ def main(argv: list[str] | None = None) -> int:
116
+ p = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
117
+ g = p.add_mutually_exclusive_group(required=True)
118
+ g.add_argument("--stdin", action="store_true", help="read draft from stdin")
119
+ g.add_argument("--file", type=Path, help="read draft from file")
120
+ g.add_argument("--scan-dir", type=Path, help="scan dir for legacy inline tags")
121
+ p.add_argument("--strict", action="store_true",
122
+ help="numbered options REQUIRE recommendation line (rule 5)")
123
+ p.add_argument("-v", "--verbose", action="store_true")
124
+ args = p.parse_args(argv)
125
+
126
+ if args.scan_dir:
127
+ return cmd_scan_dir(args.scan_dir)
128
+
129
+ text = sys.stdin.read() if args.stdin else args.file.read_text(encoding="utf-8")
130
+ code, msg = validate(text, strict=args.strict)
131
+ if code == 0:
132
+ if args.verbose:
133
+ print(f"✅ {msg}")
134
+ return 0
135
+ print(f"❌ [exit {code}] {msg}", file=sys.stderr)
136
+ return code
137
+
138
+
139
+ if __name__ == "__main__":
140
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ """Context-aware command suggestion engine.
2
+
3
+ Public API exposed for the always-on `command-suggestion` rule and for
4
+ tests. The engine is **deterministic** and **read-only**: it scores
5
+ candidate commands against a user message + recent context, applies
6
+ ranking, suppresses cooled-down suggestions, and renders a numbered
7
+ options block. It never executes a command — the user pick is what
8
+ triggers the standard slash flow.
9
+
10
+ See `agents/contexts/command-suggestion-eligibility.md` for the
11
+ locked eligibility table and `road-to-context-aware-command-suggestion`
12
+ for the full design.
13
+ """
14
+ from .types import CommandSpec, Match, Settings, CooldownState
15
+ from .loader import load_commands
16
+ from .match import match
17
+ from .rank import rank
18
+ from .cooldown import (
19
+ apply_cooldown,
20
+ CooldownStore,
21
+ detect_disable_directive,
22
+ is_explicit_slash_invocation,
23
+ )
24
+ from .render import render
25
+ from .sanitize import (
26
+ sanitize_context,
27
+ sanitize_message,
28
+ strip_code_blocks,
29
+ strip_suggestion_echo,
30
+ )
31
+ from .settings import load_settings
32
+
33
+ __all__ = [
34
+ "CommandSpec",
35
+ "Match",
36
+ "Settings",
37
+ "CooldownState",
38
+ "CooldownStore",
39
+ "load_commands",
40
+ "load_settings",
41
+ "match",
42
+ "rank",
43
+ "apply_cooldown",
44
+ "detect_disable_directive",
45
+ "is_explicit_slash_invocation",
46
+ "render",
47
+ "sanitize_context",
48
+ "sanitize_message",
49
+ "strip_code_blocks",
50
+ "strip_suggestion_echo",
51
+ ]
@@ -0,0 +1,132 @@
1
+ """Suppress recently-shown suggestions per conversation.
2
+
3
+ Cooldown key is `(command_name, evidence)` so two distinct triggers
4
+ for the same command (e.g. `/commit` from "git status shows changes"
5
+ vs. from "save this to git") track separately. The user explicitly
6
+ invoking a command via `/command` clears that command's cooldown so
7
+ the next genuine match surfaces immediately.
8
+
9
+ The store is in-memory; persistence is the agent's job (conversation
10
+ state). Phase 5 wires the per-conversation `disabled_for_conversation`
11
+ flag into the same store.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import time
17
+ from typing import Mapping
18
+
19
+ from .types import CommandSpec, CooldownState, Match, Settings
20
+
21
+
22
+ _DURATION_RE = re.compile(r"^\s*(\d+)\s*([smhd])\s*$", re.IGNORECASE)
23
+ _DISABLE_DIRECTIVE_RE = re.compile(
24
+ r"(?:^|\s)/command-suggestion-(off|on)\b", re.IGNORECASE
25
+ )
26
+ _EXPLICIT_SLASH_RE = re.compile(r"^\s*/[A-Za-z][A-Za-z0-9_-]*\b")
27
+
28
+
29
+ def is_explicit_slash_invocation(message: str) -> bool:
30
+ """Return True when the message starts with an explicit ``/command``.
31
+
32
+ Per the `command-suggestion` rule, explicit slash invocations
33
+ bypass the suggestion layer entirely \u2014 they're handled by
34
+ `slash-commands` directly. The engine should not score in that
35
+ case. Helper exposed for the runtime caller and the GT-CS4
36
+ golden.
37
+ """
38
+ if not message:
39
+ return False
40
+ return bool(_EXPLICIT_SLASH_RE.match(message))
41
+
42
+
43
+ def detect_disable_directive(message: str) -> bool | None:
44
+ """Detect a `/command-suggestion-off` / `-on` directive in the user message.
45
+
46
+ Returns ``True`` to disable for the rest of the conversation,
47
+ ``False`` to re-enable, ``None`` when no directive is present.
48
+ The latest occurrence in the message wins (order-stable on tie).
49
+ Mutating the `CooldownStore` is the caller's responsibility — this
50
+ helper stays pure so tests don't have to fake time.
51
+ """
52
+ if not message:
53
+ return None
54
+ last: bool | None = None
55
+ for m in _DISABLE_DIRECTIVE_RE.finditer(message):
56
+ last = m.group(1).lower() == "off"
57
+ return last
58
+
59
+
60
+ def parse_cooldown(value: str | None, default_seconds: int) -> int:
61
+ """Convert `'10m'` / `'30s'` / `'1h'` / `'2d'` to seconds.
62
+
63
+ Returns ``default_seconds`` for any malformed or missing input —
64
+ keeping the runtime fail-soft. The schema validator caps the
65
+ string length, so we never see absurd inputs in practice.
66
+ """
67
+ if not value:
68
+ return default_seconds
69
+ m = _DURATION_RE.match(str(value))
70
+ if not m:
71
+ return default_seconds
72
+ n, unit = int(m.group(1)), m.group(2).lower()
73
+ factor = {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
74
+ return n * factor
75
+
76
+
77
+ class CooldownStore:
78
+ """Thin wrapper around `CooldownState` with time-aware helpers.
79
+
80
+ Tests inject a fixed `now` to make decay deterministic; runtime
81
+ leaves it as `time.time`.
82
+ """
83
+
84
+ def __init__(self, state: CooldownState | None = None, *, now=time.time):
85
+ self.state = state or CooldownState()
86
+ self._now = now
87
+
88
+ def is_cooled_down(
89
+ self, command: str, evidence: str, *, window_seconds: int
90
+ ) -> bool:
91
+ last = self.state.last_shown.get((command, evidence))
92
+ if last is None:
93
+ return False
94
+ return (self._now() - last) < window_seconds
95
+
96
+ def record_shown(self, matches: list[Match]) -> None:
97
+ ts = self._now()
98
+ for m in matches:
99
+ self.state.last_shown[(m.command, m.evidence)] = ts
100
+
101
+ def record_explicit_invocation(self, command: str) -> None:
102
+ """Clear the cooldown when the user explicitly types `/command`.
103
+
104
+ We drop every entry for that command (across all evidences)
105
+ so a deliberate invocation always produces a clean slate.
106
+ """
107
+ ts = self._now()
108
+ self.state.explicit_invocations[command] = ts
109
+ keys_to_drop = [
110
+ k for k in self.state.last_shown if k[0] == command
111
+ ]
112
+ for k in keys_to_drop:
113
+ del self.state.last_shown[k]
114
+
115
+
116
+ def apply_cooldown(
117
+ matches: list[Match],
118
+ store: CooldownStore,
119
+ settings: Settings,
120
+ specs_by_name: Mapping[str, CommandSpec],
121
+ ) -> list[Match]:
122
+ if store.state.disabled_for_conversation:
123
+ return []
124
+ out: list[Match] = []
125
+ for m in matches:
126
+ spec = specs_by_name.get(m.command)
127
+ per_cmd = spec.cooldown if spec else None
128
+ window = parse_cooldown(per_cmd, settings.cooldown_seconds)
129
+ if store.is_cooled_down(m.command, m.evidence, window_seconds=window):
130
+ continue
131
+ out.append(m)
132
+ return out
@@ -0,0 +1,70 @@
1
+ """Read command frontmatter into `CommandSpec` instances.
2
+
3
+ Reuses the package's stdlib-only `validate_frontmatter.parse_frontmatter`
4
+ so the loader and the linter agree on what counts as well-formed.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .types import CommandSpec
13
+
14
+ # Sibling stdlib parser — same one the linter calls.
15
+ _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
16
+ sys.path.insert(0, str(_SCRIPTS_DIR))
17
+ from validate_frontmatter import parse_frontmatter # noqa: E402
18
+
19
+
20
+ def load_commands(commands_dir: Path) -> list[CommandSpec]:
21
+ """Load every `*.md` under ``commands_dir`` as a `CommandSpec`.
22
+
23
+ Files without a `suggestion` block are loaded as `eligible=False`
24
+ with empty rationale — keeps tests deterministic on legacy data.
25
+ Bad frontmatter is skipped silently; the linter is the gate, not
26
+ this loader.
27
+ """
28
+ specs: list[CommandSpec] = []
29
+ for path in sorted(commands_dir.glob("*.md")):
30
+ text = path.read_text(encoding="utf-8")
31
+ data, _offset = parse_frontmatter(text)
32
+ if data is None:
33
+ continue
34
+ name = str(data.get("name") or path.stem)
35
+ description = str(data.get("description") or "")
36
+ spec = _spec_from_data(name, description, data.get("suggestion"))
37
+ specs.append(spec)
38
+ return specs
39
+
40
+
41
+ def _spec_from_data(
42
+ name: str, description: str, suggestion: Any
43
+ ) -> CommandSpec:
44
+ if not isinstance(suggestion, dict):
45
+ return CommandSpec(name=name, description=description, eligible=False)
46
+ eligible = suggestion.get("eligible") is True
47
+ if not eligible:
48
+ return CommandSpec(
49
+ name=name,
50
+ description=description,
51
+ eligible=False,
52
+ rationale=str(suggestion.get("rationale") or ""),
53
+ )
54
+ floor = suggestion.get("confidence_floor")
55
+ floor_f: float | None
56
+ try:
57
+ floor_f = float(floor) if floor is not None else None
58
+ except (TypeError, ValueError):
59
+ floor_f = None
60
+ cooldown = suggestion.get("cooldown")
61
+ cooldown_s = str(cooldown) if cooldown is not None else None
62
+ return CommandSpec(
63
+ name=name,
64
+ description=description,
65
+ eligible=True,
66
+ trigger_description=str(suggestion.get("trigger_description") or ""),
67
+ trigger_context=str(suggestion.get("trigger_context") or ""),
68
+ confidence_floor=floor_f,
69
+ cooldown=cooldown_s,
70
+ )