@event4u/agent-config 1.16.0 → 1.18.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 (224) hide show
  1. package/.agent-src/commands/{agents-audit.md → agents/audit.md} +4 -3
  2. package/.agent-src/commands/{agents-cleanup.md → agents/cleanup.md} +12 -6
  3. package/.agent-src/commands/{agents-prepare.md → agents/prepare.md} +4 -3
  4. package/.agent-src/commands/agents.md +46 -0
  5. package/.agent-src/commands/{chat-history-checkpoint.md → chat-history/checkpoint.md} +4 -4
  6. package/.agent-src/commands/{chat-history-clear.md → chat-history/clear.md} +4 -4
  7. package/.agent-src/commands/{chat-history-resume.md → chat-history/resume.md} +4 -4
  8. package/.agent-src/commands/chat-history/show.md +107 -0
  9. package/.agent-src/commands/chat-history.md +33 -89
  10. package/.agent-src/commands/{commit-in-chunks.md → commit/in-chunks.md} +15 -13
  11. package/.agent-src/commands/commit.md +22 -2
  12. package/.agent-src/commands/{context-create.md → context/create.md} +4 -3
  13. package/.agent-src/commands/{context-refactor.md → context/refactor.md} +4 -3
  14. package/.agent-src/commands/context.md +44 -0
  15. package/.agent-src/commands/{copilot-agents-init.md → copilot-agents/init.md} +4 -3
  16. package/.agent-src/commands/{copilot-agents-optimize.md → copilot-agents/optimize.md} +4 -3
  17. package/.agent-src/commands/copilot-agents.md +44 -0
  18. package/.agent-src/commands/council/default.md +221 -0
  19. package/.agent-src/commands/{council-design.md → council/design.md} +6 -5
  20. package/.agent-src/commands/{council-optimize.md → council/optimize.md} +7 -6
  21. package/.agent-src/commands/{council-pr.md → council/pr.md} +6 -5
  22. package/.agent-src/commands/council.md +47 -212
  23. package/.agent-src/commands/{create-pr-description.md → create-pr/description-only.md} +4 -2
  24. package/.agent-src/commands/create-pr.md +26 -5
  25. package/.agent-src/commands/{feature-dev.md → feature/dev.md} +5 -10
  26. package/.agent-src/commands/{feature-explore.md → feature/explore.md} +4 -8
  27. package/.agent-src/commands/{feature-plan.md → feature/plan.md} +4 -8
  28. package/.agent-src/commands/{feature-refactor.md → feature/refactor.md} +4 -8
  29. package/.agent-src/commands/{feature-roadmap.md → feature/roadmap.md} +6 -10
  30. package/.agent-src/commands/feature.md +6 -12
  31. package/.agent-src/commands/{fix-ci.md → fix/ci.md} +4 -8
  32. package/.agent-src/commands/{fix-portability.md → fix/portability.md} +4 -8
  33. package/.agent-src/commands/{fix-pr-bot-comments.md → fix/pr-bots.md} +4 -8
  34. package/.agent-src/commands/{fix-pr-developer-comments.md → fix/pr-developers.md} +4 -8
  35. package/.agent-src/commands/{fix-pr-comments.md → fix/pr.md} +7 -11
  36. package/.agent-src/commands/{fix-references.md → fix/refs.md} +4 -8
  37. package/.agent-src/commands/{fix-seeder.md → fix/seeder.md} +4 -8
  38. package/.agent-src/commands/fix.md +7 -13
  39. package/.agent-src/commands/{do-and-judge.md → judge/on-diff.md} +4 -3
  40. package/.agent-src/commands/judge/solo.md +90 -0
  41. package/.agent-src/commands/{do-in-steps.md → judge/steps.md} +4 -3
  42. package/.agent-src/commands/judge.md +35 -70
  43. package/.agent-src/commands/{memory-add.md → memory/add.md} +4 -3
  44. package/.agent-src/commands/{memory-full.md → memory/load.md} +4 -3
  45. package/.agent-src/commands/{memory-promote.md → memory/promote.md} +4 -3
  46. package/.agent-src/commands/{propose-memory.md → memory/propose.md} +4 -3
  47. package/.agent-src/commands/memory.md +48 -0
  48. package/.agent-src/commands/{module-create.md → module/create.md} +4 -3
  49. package/.agent-src/commands/{module-explore.md → module/explore.md} +4 -3
  50. package/.agent-src/commands/module.md +44 -0
  51. package/.agent-src/commands/{optimize-agents.md → optimize/agents.md} +4 -8
  52. package/.agent-src/commands/{optimize-augmentignore.md → optimize/augmentignore.md} +4 -9
  53. package/.agent-src/commands/{optimize-rtk-filters.md → optimize/rtk.md} +4 -8
  54. package/.agent-src/commands/{optimize-skills.md → optimize/skills.md} +4 -8
  55. package/.agent-src/commands/optimize.md +4 -10
  56. package/.agent-src/commands/{override-create.md → override/create.md} +4 -3
  57. package/.agent-src/commands/{override-manage.md → override/manage.md} +4 -3
  58. package/.agent-src/commands/override.md +44 -0
  59. package/.agent-src/commands/{roadmap-create.md → roadmap/create.md} +4 -3
  60. package/.agent-src/commands/{roadmap-execute.md → roadmap/execute.md} +4 -3
  61. package/.agent-src/commands/roadmap.md +44 -0
  62. package/.agent-src/commands/{tests-create.md → tests/create.md} +4 -3
  63. package/.agent-src/commands/{tests-execute.md → tests/execute.md} +4 -3
  64. package/.agent-src/commands/tests.md +44 -0
  65. package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +72 -0
  66. package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +79 -0
  67. package/.agent-src/contexts/communication/rules-auto/augment-source-of-truth-mechanics.md +98 -0
  68. package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +87 -0
  69. package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +62 -0
  70. package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +78 -0
  71. package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +85 -0
  72. package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +65 -0
  73. package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +78 -0
  74. package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +62 -0
  75. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +55 -0
  76. package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +53 -0
  77. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +77 -0
  78. package/.agent-src/contexts/judges/no-consolidate-rationale.md +102 -0
  79. package/.agent-src/contexts/judges/persona-voice-rubric.md +140 -0
  80. package/.agent-src/rules/artifact-engagement-recording.md +13 -69
  81. package/.agent-src/rules/ask-when-uncertain.md +27 -42
  82. package/.agent-src/rules/augment-portability.md +15 -61
  83. package/.agent-src/rules/augment-source-of-truth.md +27 -93
  84. package/.agent-src/rules/cli-output-handling.md +10 -76
  85. package/.agent-src/rules/command-suggestion-policy.md +18 -59
  86. package/.agent-src/rules/commit-conventions.md +17 -14
  87. package/.agent-src/rules/context-hygiene.md +6 -0
  88. package/.agent-src/rules/direct-answers.md +35 -59
  89. package/.agent-src/rules/docker-commands.md +5 -5
  90. package/.agent-src/rules/docs-sync.md +15 -69
  91. package/.agent-src/rules/language-and-tone.md +48 -72
  92. package/.agent-src/rules/missing-tool-handling.md +28 -22
  93. package/.agent-src/rules/no-cheap-questions.md +39 -53
  94. package/.agent-src/rules/no-roadmap-references.md +73 -0
  95. package/.agent-src/rules/onboarding-gate.md +7 -0
  96. package/.agent-src/rules/package-ci-checks.md +21 -61
  97. package/.agent-src/rules/preservation-guard.md +64 -29
  98. package/.agent-src/rules/review-routing-awareness.md +24 -43
  99. package/.agent-src/rules/roadmap-progress-sync.md +31 -65
  100. package/.agent-src/rules/rule-type-governance.md +28 -0
  101. package/.agent-src/rules/security-sensitive-stop.md +8 -8
  102. package/.agent-src/rules/skill-quality.md +16 -48
  103. package/.agent-src/rules/slash-command-routing-policy.md +7 -4
  104. package/.agent-src/rules/think-before-action.md +52 -42
  105. package/.agent-src/rules/tool-safety.md +19 -16
  106. package/.agent-src/rules/ui-audit-gate.md +24 -38
  107. package/.agent-src/rules/user-interaction.md +13 -68
  108. package/.agent-src/skills/ai-council/SKILL.md +2 -0
  109. package/.agent-src/skills/api-testing/SKILL.md +1 -1
  110. package/.agent-src/skills/check-refs/SKILL.md +59 -40
  111. package/.agent-src/skills/conventional-commits-writing/SKILL.md +86 -28
  112. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +5 -5
  113. package/.agent-src/skills/developer-like-execution/SKILL.md +4 -4
  114. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +101 -65
  115. package/.agent-src/skills/flux/SKILL.md +30 -10
  116. package/.agent-src/skills/github-ci/SKILL.md +2 -2
  117. package/.agent-src/skills/judge-code-quality/SKILL.md +7 -8
  118. package/.agent-src/skills/judge-security-auditor/SKILL.md +4 -5
  119. package/.agent-src/skills/judge-test-coverage/SKILL.md +3 -4
  120. package/.agent-src/skills/lint-skills/SKILL.md +57 -39
  121. package/.agent-src/skills/md-language-check/SKILL.md +61 -39
  122. package/.agent-src/skills/override-management/SKILL.md +5 -5
  123. package/.agent-src/skills/quality-tools/SKILL.md +2 -2
  124. package/.agent-src/skills/react-shadcn-ui/SKILL.md +116 -43
  125. package/.agent-src/skills/readme-reviewer/SKILL.md +30 -29
  126. package/.agent-src/skills/readme-writing/SKILL.md +78 -53
  127. package/.agent-src/skills/readme-writing-package/SKILL.md +50 -47
  128. package/.agent-src/skills/receiving-code-review/SKILL.md +52 -47
  129. package/.agent-src/skills/refine-prompt/SKILL.md +0 -1
  130. package/.agent-src/skills/requesting-code-review/SKILL.md +35 -30
  131. package/.agent-src/skills/security/SKILL.md +7 -2
  132. package/.agent-src/skills/security-audit/SKILL.md +7 -3
  133. package/.agent-src/skills/systematic-debugging/SKILL.md +68 -60
  134. package/.agent-src/skills/test-driven-development/SKILL.md +59 -57
  135. package/.agent-src/skills/test-performance/SKILL.md +0 -1
  136. package/.agent-src/skills/traefik/SKILL.md +4 -4
  137. package/.agent-src/skills/verify-completion-evidence/SKILL.md +28 -26
  138. package/.agent-src/templates/roadmaps.md +4 -0
  139. package/.claude-plugin/marketplace.json +22 -11
  140. package/AGENTS.md +2 -2
  141. package/CHANGELOG.md +125 -1
  142. package/README.md +18 -17
  143. package/docs/architecture.md +4 -6
  144. package/docs/catalog.md +67 -39
  145. package/docs/contracts/STABILITY.md +13 -7
  146. package/docs/contracts/adr-chat-history-split.md +1 -3
  147. package/docs/contracts/adr-command-suggestion.md +0 -2
  148. package/docs/contracts/adr-implement-ticket-runtime.md +1 -2
  149. package/docs/contracts/adr-product-ui-track.md +3 -6
  150. package/docs/contracts/adr-prompt-driven-execution.md +3 -4
  151. package/docs/contracts/agent-memory-contract.md +6 -11
  152. package/docs/contracts/artifact-engagement-flow.md +6 -9
  153. package/docs/contracts/command-clusters.md +56 -46
  154. package/docs/contracts/command-suggestion-flow.md +1 -3
  155. package/docs/contracts/context-paths.md +99 -0
  156. package/docs/contracts/file-ownership-matrix.json +6722 -0
  157. package/docs/contracts/file-ownership-matrix.md +134 -0
  158. package/docs/contracts/implement-ticket-flow.md +6 -9
  159. package/docs/contracts/linear-ai-rules-inclusion.md +0 -1
  160. package/docs/contracts/linear-ai-three-layers.md +0 -2
  161. package/docs/contracts/load-context-budget-model.md +258 -0
  162. package/docs/contracts/load-context-schema.md +21 -3
  163. package/docs/contracts/roadmap-complexity-standard.md +137 -0
  164. package/docs/contracts/rule-interactions.md +0 -1
  165. package/docs/contracts/rule-priority-hierarchy.md +1 -1
  166. package/docs/contracts/ui-track-flow.md +7 -17
  167. package/docs/customization.md +2 -0
  168. package/docs/getting-started.md +5 -4
  169. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
  170. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +100 -0
  171. package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
  172. package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
  173. package/package.json +1 -1
  174. package/scripts/_phase2_shim_helper.py +109 -0
  175. package/scripts/agent-config +30 -0
  176. package/scripts/ai_council/one_off_archive/2026-05/README.md +45 -0
  177. package/scripts/ai_council/one_off_archive/2026-05/_one_off_2a4_acceptance.py +208 -0
  178. package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
  179. package/scripts/ai_council/one_off_archive/2026-05/_one_off_context_layer_v1_estimate.py +67 -0
  180. package/scripts/ai_council/one_off_archive/2026-05/_one_off_context_layer_v1_review.py +292 -0
  181. package/scripts/ai_council/one_off_archive/2026-05/_one_off_followups_review.py +259 -0
  182. package/scripts/ai_council/one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py +209 -0
  183. package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py +108 -0
  184. package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py +92 -0
  185. package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase_2a_budget_rebalance.py +257 -0
  186. package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase_2a_post_revert.py +197 -0
  187. package/scripts/ai_council/one_off_archive/2026-05/_one_off_rule_hardening_v1.py +251 -0
  188. package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_open_questions.py +232 -0
  189. package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_optimization.py +144 -0
  190. package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_v3_gaps.py +252 -0
  191. package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_v3_review.py +240 -0
  192. package/scripts/build_rule_trigger_matrix.py +360 -0
  193. package/scripts/check_always_budget.py +402 -45
  194. package/scripts/check_cluster_patterns.py +159 -0
  195. package/scripts/check_command_count_messaging.py +14 -7
  196. package/scripts/check_context_paths.py +201 -0
  197. package/scripts/check_no_roadmap_refs.py +155 -0
  198. package/scripts/check_one_off_location.py +81 -0
  199. package/scripts/check_phase_coupling.py +148 -0
  200. package/scripts/check_portability.py +2 -0
  201. package/scripts/check_references.py +35 -2
  202. package/scripts/check_safety_floor_untouched.py +125 -0
  203. package/scripts/command_suggester/loader.py +4 -1
  204. package/scripts/compress.py +64 -15
  205. package/scripts/context_hygiene_hook.py +173 -0
  206. package/scripts/generate_index.py +6 -2
  207. package/scripts/generate_ownership_matrix.py +323 -0
  208. package/scripts/hooks/augment-context-hygiene.sh +55 -0
  209. package/scripts/hooks/augment-onboarding-gate.sh +55 -0
  210. package/scripts/hooks/augment-roadmap-progress.sh +57 -0
  211. package/scripts/install.py +105 -45
  212. package/scripts/lint_examples.py +98 -0
  213. package/scripts/lint_no_new_atomic_commands.py +12 -11
  214. package/scripts/lint_roadmap_complexity.py +127 -0
  215. package/scripts/onboarding_gate_hook.py +137 -0
  216. package/scripts/requirements-evals.txt +1 -0
  217. package/scripts/roadmap_progress_hook.py +159 -0
  218. package/scripts/schemas/command.schema.json +4 -3
  219. package/scripts/schemas/rule.schema.json +5 -0
  220. package/scripts/skill_linter.py +1 -0
  221. package/scripts/sync_agent_settings.py +25 -2
  222. package/scripts/update_counts.py +7 -0
  223. /package/scripts/ai_council/{_one_off_rebalancing_audit.py → one_off_archive/2026-05/_one_off_rebalancing_audit.py} +0 -0
  224. /package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +0 -0
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 3.4 demo-shape linter — wrong / right / why per demo.
3
+
4
+ Cap: ≤ 100 LOC, stdlib only. Hooked into `task ci` via
5
+ `Taskfile.yml` ▸ `check-examples-shape`. Validates every
6
+ `docs/guidelines/agent-infra/*-demos.md`: frontmatter keys
7
+ (`demo_for:`, `layer: pattern-memory`, `prose_delta:` with before /
8
+ after char counts), and each `## Demo N` section having Wrong /
9
+ Right shape headings, a `**Failure mode:**` line, and a Why-it-works
10
+ explanation (heading or inline).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ REPO_ROOT = Path(__file__).resolve().parent.parent
19
+ DEMO_GLOB = "docs/guidelines/agent-infra/*-demos.md"
20
+ REQUIRED_FM_KEYS = ("demo_for:", "layer: pattern-memory", "prose_delta:")
21
+ REQUIRED_FM_DELTA = ("rule_chars_before:", "rule_chars_after:")
22
+
23
+
24
+ def _frontmatter(text: str) -> str:
25
+ if not text.startswith("---\n"):
26
+ return ""
27
+ end = text.find("\n---\n", 4)
28
+ return text[4:end] if end != -1 else ""
29
+
30
+
31
+ def _check_frontmatter(fm: str, problems: list[str]) -> None:
32
+ for key in (*REQUIRED_FM_KEYS, *REQUIRED_FM_DELTA):
33
+ if key not in fm:
34
+ problems.append(f"frontmatter missing: {key!r}")
35
+
36
+
37
+ def _check_demo_sections(text: str, problems: list[str]) -> None:
38
+ demo_pat = re.compile(r"^## Demo \d+\b.*$", re.MULTILINE)
39
+ demo_starts = [m.start() for m in demo_pat.finditer(text)]
40
+ if not demo_starts:
41
+ problems.append("no '## Demo N — …' sections found")
42
+ return
43
+ bounds = demo_starts + [len(text)]
44
+ for i, start in enumerate(demo_starts):
45
+ section = text[start:bounds[i + 1]]
46
+ title = section.splitlines()[0]
47
+ if "### Wrong shape" not in section:
48
+ problems.append(f"{title!r}: missing '### Wrong shape'")
49
+ if "### Right shape" not in section:
50
+ problems.append(f"{title!r}: missing '### Right shape'")
51
+ if "**Failure mode:**" not in section:
52
+ problems.append(f"{title!r}: missing '**Failure mode:**' line")
53
+ has_why_section = "### Why it works" in section
54
+ has_why_inline = "**Why it works:**" in section
55
+ if not (has_why_section or has_why_inline):
56
+ problems.append(
57
+ f"{title!r}: missing 'Why it works' explanation "
58
+ "(### Why it works or **Why it works:** inline)"
59
+ )
60
+
61
+
62
+ def lint_demo(path: Path) -> list[str]:
63
+ text = path.read_text(encoding="utf-8")
64
+ problems: list[str] = []
65
+ fm = _frontmatter(text)
66
+ if not fm:
67
+ problems.append("missing YAML frontmatter (--- block at top)")
68
+ else:
69
+ _check_frontmatter(fm, problems)
70
+ _check_demo_sections(text, problems)
71
+ return problems
72
+
73
+
74
+ def main() -> int:
75
+ demos = sorted(REPO_ROOT.glob(DEMO_GLOB))
76
+ if not demos:
77
+ print(f"❌ no demo files matched {DEMO_GLOB}", file=sys.stderr)
78
+ return 1
79
+ failed = 0
80
+ for demo in demos:
81
+ rel = demo.relative_to(REPO_ROOT)
82
+ problems = lint_demo(demo)
83
+ if problems:
84
+ failed += 1
85
+ print(f"❌ {rel}", file=sys.stderr)
86
+ for p in problems:
87
+ print(f" - {p}", file=sys.stderr)
88
+ else:
89
+ print(f"✅ {rel}")
90
+ if failed:
91
+ print(f"\n❌ {failed} demo file(s) failed shape lint", file=sys.stderr)
92
+ return 1
93
+ print(f"\n✅ {len(demos)} demo file(s) shape-clean")
94
+ return 0
95
+
96
+
97
+ if __name__ == "__main__":
98
+ sys.exit(main())
@@ -43,24 +43,24 @@ class Violation:
43
43
 
44
44
 
45
45
  def load_locked_clusters() -> set[str]:
46
- """Parse the Phase 1 cluster table from the locked contract."""
46
+ """Parse the locked cluster table from the contract."""
47
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
48
+ # Locate the locked-clusters table; cluster names sit in backticks in column 1.
49
+ in_table = False
50
50
  clusters: set[str] = set()
51
51
  for line in text.splitlines():
52
- if line.startswith("## Phase 1 clusters"):
53
- in_phase_1 = True
52
+ if line.startswith("## Locked clusters"):
53
+ in_table = True
54
54
  continue
55
- if in_phase_1 and line.startswith("## "):
55
+ if in_table and line.startswith("## "):
56
56
  break
57
- if in_phase_1:
57
+ if in_table:
58
58
  m = re.match(r"\|\s*`([a-z][a-z0-9-]*)`\s*\|", line)
59
59
  if m:
60
60
  clusters.add(m.group(1))
61
61
  if not clusters:
62
62
  print(
63
- f"❌ Could not parse Phase 1 cluster table from {CLUSTER_CONTRACT}",
63
+ f"❌ Could not parse locked-clusters table from {CLUSTER_CONTRACT}",
64
64
  file=sys.stderr,
65
65
  )
66
66
  sys.exit(3)
@@ -83,7 +83,7 @@ def added_command_files(baseline: str) -> list[Path]:
83
83
  file=sys.stderr)
84
84
  sys.exit(3)
85
85
  files = [Path(p) for p in result.stdout.splitlines()
86
- if p.endswith(".md") and p != ""]
86
+ if p.endswith(".md") and p != "" and Path(p).name != "AGENTS.md"]
87
87
  # Also include untracked (newly added, uncommitted) files.
88
88
  try:
89
89
  wt = subprocess.run(
@@ -97,7 +97,7 @@ def added_command_files(baseline: str) -> list[Path]:
97
97
  if status.strip() not in ("A", "??", "AM"):
98
98
  continue
99
99
  path = line[3:].strip().split(" -> ")[-1]
100
- if path.endswith(".md"):
100
+ if path.endswith(".md") and Path(path).name != "AGENTS.md":
101
101
  p = Path(path)
102
102
  if p not in files:
103
103
  files.append(p)
@@ -107,7 +107,8 @@ def added_command_files(baseline: str) -> list[Path]:
107
107
 
108
108
 
109
109
  def all_command_files() -> list[Path]:
110
- return sorted((ROOT / COMMANDS_DIR).glob("*.md"))
110
+ return sorted(p for p in (ROOT / COMMANDS_DIR).rglob("*.md")
111
+ if p.name != "AGENTS.md")
111
112
 
112
113
 
113
114
  def parse_frontmatter(path: Path) -> dict[str, str]:
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 5.2 roadmap-complexity linter.
3
+
4
+ Enforces the measurable subset of
5
+ `docs/contracts/roadmap-complexity-standard.md`:
6
+
7
+ - every `agents/roadmaps/*.md` declares `complexity: lightweight`
8
+ or `complexity: structural` in frontmatter;
9
+ - lightweight roadmaps have ≤ 600 total lines and ≤ 6 `## Phase N`
10
+ headings, and contain no `## Council Round N` / `### Verdict`
11
+ sections;
12
+ - structural roadmaps have no upper cap, but the tag must be
13
+ declared.
14
+
15
+ Cap: ≤ 150 LOC, stdlib only. Hooked into `task ci` via
16
+ `task lint-roadmap-complexity`.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ REPO_ROOT = Path(__file__).resolve().parent.parent
25
+ ROADMAP_GLOB = "agents/roadmaps/*.md"
26
+ LIGHTWEIGHT_LINE_CAP = 600
27
+ LIGHTWEIGHT_PHASE_CAP = 6
28
+
29
+ PHASE_PAT = re.compile(r"^## Phase \d+\b", re.MULTILINE)
30
+ COUNCIL_PAT = re.compile(r"^## Council Round \d+\b", re.MULTILINE)
31
+ VERDICT_PAT = re.compile(r"^### Verdict\b", re.MULTILINE)
32
+ COMPLEXITY_PAT = re.compile(
33
+ r"^complexity:\s*(lightweight|structural)\s*$", re.MULTILINE
34
+ )
35
+
36
+
37
+ def _frontmatter(text: str) -> str:
38
+ if not text.startswith("---\n"):
39
+ return ""
40
+ end = text.find("\n---\n", 4)
41
+ return text[4:end] if end != -1 else ""
42
+
43
+
44
+ def _read_complexity(fm: str) -> str | None:
45
+ m = COMPLEXITY_PAT.search(fm)
46
+ return m.group(1) if m else None
47
+
48
+
49
+ def _check_lightweight(text: str, line_count: int, problems: list[str]) -> None:
50
+ if line_count > LIGHTWEIGHT_LINE_CAP:
51
+ problems.append(
52
+ f"lightweight cap exceeded: {line_count} lines "
53
+ f"(max {LIGHTWEIGHT_LINE_CAP}); consider tagging structural "
54
+ f"or trimming"
55
+ )
56
+ phases = len(PHASE_PAT.findall(text))
57
+ if phases > LIGHTWEIGHT_PHASE_CAP:
58
+ problems.append(
59
+ f"lightweight phase cap exceeded: {phases} phases "
60
+ f"(max {LIGHTWEIGHT_PHASE_CAP})"
61
+ )
62
+ if COUNCIL_PAT.search(text):
63
+ problems.append(
64
+ "lightweight roadmap contains '## Council Round N' "
65
+ "block — council debates belong in structural roadmaps"
66
+ )
67
+ if VERDICT_PAT.search(text):
68
+ problems.append(
69
+ "lightweight roadmap contains '### Verdict' block — "
70
+ "council verdicts belong in structural roadmaps"
71
+ )
72
+
73
+
74
+ def lint_roadmap(path: Path) -> list[str]:
75
+ text = path.read_text(encoding="utf-8")
76
+ line_count = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
77
+ problems: list[str] = []
78
+ fm = _frontmatter(text)
79
+ complexity = _read_complexity(fm) if fm else None
80
+ if complexity is None:
81
+ problems.append(
82
+ "missing 'complexity:' frontmatter "
83
+ "(must declare 'lightweight' or 'structural')"
84
+ )
85
+ return problems
86
+ if complexity == "lightweight":
87
+ _check_lightweight(text, line_count, problems)
88
+ return problems
89
+
90
+
91
+ def main() -> int:
92
+ roadmaps = sorted(REPO_ROOT.glob(ROADMAP_GLOB))
93
+ if not roadmaps:
94
+ print(f"❌ no roadmaps matched {ROADMAP_GLOB}", file=sys.stderr)
95
+ return 1
96
+ failed = 0
97
+ summary: list[tuple[str, str]] = []
98
+ for roadmap in roadmaps:
99
+ rel = roadmap.relative_to(REPO_ROOT)
100
+ problems = lint_roadmap(roadmap)
101
+ text = roadmap.read_text(encoding="utf-8")
102
+ complexity = _read_complexity(_frontmatter(text)) or "untagged"
103
+ summary.append((str(rel), complexity))
104
+ if problems:
105
+ failed += 1
106
+ print(f"❌ {rel} [{complexity}]", file=sys.stderr)
107
+ for p in problems:
108
+ print(f" - {p}", file=sys.stderr)
109
+ else:
110
+ print(f"✅ {rel} [{complexity}]")
111
+ print()
112
+ light = sum(1 for _, c in summary if c == "lightweight")
113
+ structural = sum(1 for _, c in summary if c == "structural")
114
+ untagged = sum(1 for _, c in summary if c == "untagged")
115
+ print(
116
+ f"summary: {light} lightweight · {structural} structural · "
117
+ f"{untagged} untagged · {len(summary)} total"
118
+ )
119
+ if failed:
120
+ print(f"\n❌ {failed} roadmap(s) failed complexity lint", file=sys.stderr)
121
+ return 1
122
+ print(f"\n✅ {len(roadmaps)} roadmap(s) complexity-clean")
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """Platform-agnostic hook for the `onboarding-gate` rule.
3
+
4
+ Reads `.agent-settings.yml` from the consumer repo and writes a
5
+ deterministic state file the rule body can cite as the source of
6
+ truth for "do I need to prompt the user about /onboard?".
7
+
8
+ Output is written to `agents/state/onboarding-gate.json` with:
9
+ {
10
+ "required": <bool>, // true → rule fires on first turn
11
+ "reason": "<string>", // why this state was set
12
+ "checked_at": "<iso8601>", // last hook run
13
+ "settings_present": <bool> // .agent-settings.yml exists
14
+ }
15
+
16
+ Exit code is **always 0**. Hooks must never block the agent loop.
17
+
18
+ Output discipline:
19
+ - stdout: nothing (Augment surfaces stdout to the user)
20
+ - stderr: one short line in --verbose mode, otherwise silent
21
+
22
+ CLI:
23
+ python3 scripts/onboarding_gate_hook.py [--platform NAME] [--verbose]
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import datetime as _dt
29
+ import json
30
+ import re
31
+ import sys
32
+ from pathlib import Path
33
+
34
+ SETTINGS_FILE = ".agent-settings.yml"
35
+ STATE_DIR = Path("agents") / "state"
36
+ STATE_FILE = STATE_DIR / "onboarding-gate.json"
37
+
38
+
39
+ def _read_onboarded(settings_path: Path) -> tuple[bool, str]:
40
+ """Return (required, reason) — minimal, dependency-free YAML parsing.
41
+
42
+ We only need a single key under the `onboarding:` block. Full YAML is
43
+ overkill (and would pull in a runtime dep). We scan line-by-line for
44
+ `onboarded: <bool>` inside the `onboarding:` section.
45
+ """
46
+ if not settings_path.is_file():
47
+ return (False, "settings_file_missing") # legacy: do not block
48
+
49
+ try:
50
+ text = settings_path.read_text(encoding="utf-8")
51
+ except OSError:
52
+ return (False, "settings_file_unreadable")
53
+
54
+ in_onboarding = False
55
+ onboarded_value: str | None = None
56
+ for raw in text.splitlines():
57
+ line = raw.rstrip()
58
+ if not line or line.lstrip().startswith("#"):
59
+ continue
60
+ if re.match(r"^onboarding\s*:\s*$", line):
61
+ in_onboarding = True
62
+ continue
63
+ if in_onboarding:
64
+ # Section ends when a top-level (non-indented) key starts.
65
+ if line and not line.startswith((" ", "\t")):
66
+ break
67
+ m = re.match(r"^\s+onboarded\s*:\s*(\S+)\s*(?:#.*)?$", line)
68
+ if m:
69
+ onboarded_value = m.group(1).strip().lower()
70
+
71
+ if onboarded_value is None:
72
+ return (False, "key_missing") # legacy / pre-rule project
73
+ if onboarded_value in ("true", "yes", "on"):
74
+ return (False, "already_onboarded")
75
+ if onboarded_value in ("false", "no", "off"):
76
+ return (True, "explicit_false")
77
+ return (False, f"unknown_value:{onboarded_value}")
78
+
79
+
80
+ def _write_state(consumer_root: Path, required: bool, reason: str,
81
+ settings_present: bool) -> None:
82
+ """Write `agents/state/onboarding-gate.json` atomically."""
83
+ state_dir = consumer_root / STATE_DIR
84
+ state_dir.mkdir(parents=True, exist_ok=True)
85
+ payload = {
86
+ "required": required,
87
+ "reason": reason,
88
+ "checked_at": _dt.datetime.now(_dt.timezone.utc).isoformat(
89
+ timespec="seconds"),
90
+ "settings_present": settings_present,
91
+ }
92
+ target = consumer_root / STATE_FILE
93
+ tmp = target.with_suffix(".json.tmp")
94
+ tmp.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
95
+ tmp.replace(target)
96
+
97
+
98
+ def run(*, consumer_root: Path, verbose: bool = False) -> int:
99
+ settings_path = consumer_root / SETTINGS_FILE
100
+ settings_present = settings_path.is_file()
101
+ try:
102
+ required, reason = _read_onboarded(settings_path)
103
+ except Exception: # pragma: no cover — defensive
104
+ required, reason = (False, "hook_error")
105
+
106
+ try:
107
+ _write_state(consumer_root, required, reason, settings_present)
108
+ except OSError:
109
+ if verbose:
110
+ print("onboarding-gate-hook: state write failed",
111
+ file=sys.stderr)
112
+ return 0 # never block
113
+
114
+ if verbose:
115
+ print(f"onboarding-gate-hook: required={required} reason={reason}",
116
+ file=sys.stderr)
117
+ return 0
118
+
119
+
120
+ def main(argv: list[str] | None = None) -> int:
121
+ parser = argparse.ArgumentParser(description=__doc__)
122
+ parser.add_argument("--platform", default="generic",
123
+ help="informational platform tag")
124
+ parser.add_argument("--verbose", action="store_true",
125
+ help="emit one stderr line per invocation")
126
+ args = parser.parse_args(argv)
127
+ # Drain stdin so callers piping JSON don't block on a SIGPIPE on
128
+ # platforms that strictly require stdin to be consumed.
129
+ try:
130
+ sys.stdin.read()
131
+ except Exception:
132
+ pass
133
+ return run(consumer_root=Path.cwd(), verbose=args.verbose)
134
+
135
+
136
+ if __name__ == "__main__": # pragma: no cover
137
+ sys.exit(main())
@@ -5,3 +5,4 @@
5
5
  # be upgraded, bump the version here and rerun the setup script.
6
6
 
7
7
  anthropic==0.96.0
8
+ openai==1.109.1
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """Platform-agnostic PostToolUse hook for the `roadmap-progress-sync` rule.
3
+
4
+ Reads a JSON event from stdin (Augment / Claude / Cursor / Cline /
5
+ Windsurf / Gemini PostToolUse-shaped envelopes), decides whether the
6
+ tool call wrote to a roadmap file under `agents/roadmaps/`, and — when
7
+ it did — re-runs `update_roadmap_progress.py` so the dashboard stays
8
+ in sync without depending on agent self-discipline.
9
+
10
+ Exit code is **always 0**. Hooks must never block the agent loop; the
11
+ worst-case is a no-op when stdin is malformed or the regenerator is
12
+ missing.
13
+
14
+ Output discipline:
15
+ - stdout: nothing (Augment would surface stdout to the user)
16
+ - stderr: one short line in --verbose mode, otherwise silent
17
+
18
+ CLI:
19
+ python3 scripts/roadmap_progress_hook.py [--platform NAME] [--verbose]
20
+
21
+ The `--platform` flag is informational only — the filter logic reads
22
+ the same field names across platforms (tool_name, tool_input.path,
23
+ file_changes[].path).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import subprocess
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ # Tools whose successful execution can write to a roadmap file. We keep
34
+ # the list explicit so an unknown tool name (e.g. a new MCP tool that
35
+ # happens to mention a roadmap path in its input) does not trigger a
36
+ # spurious regeneration.
37
+ WRITE_TOOLS = frozenset({
38
+ "str-replace-editor",
39
+ "save-file",
40
+ "remove-files",
41
+ # Claude Code / Cursor naming variants — kept for cross-platform
42
+ # parity if this hook is ever wired beyond Augment.
43
+ "Edit",
44
+ "Write",
45
+ "MultiEdit",
46
+ })
47
+
48
+ ROADMAP_PREFIX = "agents/roadmaps/"
49
+ # Paths under these subtrees are tracked but not part of the open list
50
+ # the dashboard summarises — regenerating on every archived edit would
51
+ # be wasteful. The check still fires on the parent dir itself.
52
+ ROADMAP_EXCLUDED_PARTS = frozenset({"archive", "skipped"})
53
+ DASHBOARD_PATH = "agents/roadmaps-progress.md"
54
+
55
+
56
+ def _candidate_paths(payload: dict) -> list[str]:
57
+ """Pull every plausible file path out of a PostToolUse payload."""
58
+ out: list[str] = []
59
+ fc = payload.get("file_changes")
60
+ if isinstance(fc, list):
61
+ for entry in fc:
62
+ if isinstance(entry, dict):
63
+ p = entry.get("path")
64
+ if isinstance(p, str) and p:
65
+ out.append(p)
66
+ ti = payload.get("tool_input")
67
+ if isinstance(ti, dict):
68
+ for key in ("path", "file_path", "target_file"):
69
+ v = ti.get(key)
70
+ if isinstance(v, str) and v:
71
+ out.append(v)
72
+ return out
73
+
74
+
75
+ def _is_roadmap_touch(path: str) -> bool:
76
+ """Return True if `path` is a roadmap file we should react to."""
77
+ norm = path.lstrip("./").replace("\\", "/")
78
+ if not norm.startswith(ROADMAP_PREFIX):
79
+ return False
80
+ if norm == DASHBOARD_PATH:
81
+ # Defensive — the dashboard sits at agents/roadmaps-progress.md,
82
+ # NOT inside agents/roadmaps/. The prefix check above already
83
+ # excludes it, but keep this explicit so a future relocation
84
+ # cannot turn the hook into an infinite loop.
85
+ return False
86
+ rest = norm[len(ROADMAP_PREFIX):]
87
+ parts = rest.split("/")
88
+ if len(parts) >= 2 and parts[0] in ROADMAP_EXCLUDED_PARTS:
89
+ return False
90
+ if not norm.endswith(".md"):
91
+ return False
92
+ return True
93
+
94
+
95
+ def _resolve_regenerator(consumer_root: Path) -> Path | None:
96
+ """Find the regenerator script — package-shipped or installed copy."""
97
+ for candidate in (
98
+ consumer_root / ".augment" / "scripts" / "update_roadmap_progress.py",
99
+ consumer_root / ".agent-src" / "scripts" / "update_roadmap_progress.py",
100
+ consumer_root / ".agent-src.uncompressed" / "scripts" / "update_roadmap_progress.py",
101
+ ):
102
+ if candidate.is_file():
103
+ return candidate
104
+ return None
105
+
106
+
107
+ def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
108
+ payload: dict = {}
109
+ if stdin_text.strip():
110
+ try:
111
+ decoded = json.loads(stdin_text)
112
+ if isinstance(decoded, dict):
113
+ payload = decoded
114
+ except json.JSONDecodeError:
115
+ return 0 # malformed stdin → silent no-op, never block
116
+
117
+ tool = payload.get("tool_name") or payload.get("toolName") or payload.get("tool")
118
+ if not isinstance(tool, str) or tool not in WRITE_TOOLS:
119
+ return 0
120
+
121
+ paths = _candidate_paths(payload)
122
+ if not any(_is_roadmap_touch(p) for p in paths):
123
+ return 0
124
+
125
+ script = _resolve_regenerator(consumer_root)
126
+ if script is None:
127
+ if verbose:
128
+ print("roadmap-progress-hook: regenerator not found, skipping",
129
+ file=sys.stderr)
130
+ return 0
131
+
132
+ try:
133
+ subprocess.run(
134
+ [sys.executable, str(script)],
135
+ cwd=consumer_root, check=False,
136
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
137
+ timeout=30,
138
+ )
139
+ except (OSError, subprocess.SubprocessError):
140
+ pass # never propagate regenerator failures into the agent loop
141
+
142
+ if verbose:
143
+ print(f"roadmap-progress-hook: regenerated for tool={tool}",
144
+ file=sys.stderr)
145
+ return 0
146
+
147
+
148
+ def main(argv: list[str] | None = None) -> int:
149
+ parser = argparse.ArgumentParser(description=__doc__)
150
+ parser.add_argument("--platform", default="generic",
151
+ help="informational platform tag (augment/claude/...)")
152
+ parser.add_argument("--verbose", action="store_true",
153
+ help="emit one stderr line per invocation")
154
+ args = parser.parse_args(argv)
155
+ return run(sys.stdin.read(), consumer_root=Path.cwd(), verbose=args.verbose)
156
+
157
+
158
+ if __name__ == "__main__": # pragma: no cover
159
+ sys.exit(main())
@@ -9,7 +9,8 @@
9
9
  "properties": {
10
10
  "name": {
11
11
  "type": "string",
12
- "pattern": "^[a-z][a-z0-9-]*$"
12
+ "pattern": "^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$",
13
+ "$comment": "Top-level commands use the bare slug (`commit`). Nested cluster commands under `commands/<cluster>/<sub>.md` use the colon form (`council:default`) to mirror Claude Code's `/cluster:sub` rendering. Directory slug for `.claude/skills/` is the hyphenated form (`council-default`), generated by compress.py."
13
14
  },
14
15
  "description": {
15
16
  "type": "string",
@@ -40,8 +41,8 @@
40
41
  },
41
42
  "superseded_by": {
42
43
  "type": "string",
43
- "pattern": "^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)?$",
44
- "description": "Set on deprecation shims. Format: '<cluster> <sub>' (e.g. 'fix ci'). See docs/contracts/command-clusters.md § Deprecation shim contract."
44
+ "pattern": "^[a-z][a-z0-9-]*( (--[a-z][a-z0-9-]*|[a-z][a-z0-9-]*))?$",
45
+ "description": "Set on deprecation shims. Format: '<cluster> <sub>' (e.g. 'fix ci') or '<cluster> --<flag>' for flag-clusters (e.g. 'commit --in-chunks'). See docs/contracts/command-clusters.md § Deprecation shim contract."
45
46
  },
46
47
  "deprecated_in": {
47
48
  "type": "string",
@@ -33,6 +33,11 @@
33
33
  "type": "array",
34
34
  "items": {"type": "string", "pattern": "\\.md$"},
35
35
  "description": "Eager auto-loaded context references. Counts against the per-rule char budget; enforced by scripts/lint_load_context.py."
36
+ },
37
+ "tier": {
38
+ "type": "string",
39
+ "enum": ["1", "2a", "2b", "3", "safety-floor", "mechanical-already"],
40
+ "description": "Hardening tier per road-to-rule-hardening.md. Optional today, recommended for new rules. Tracked in agents/contexts/rule-trigger-matrix.md. Tier 3 rules also referenced in agents/contexts/tier-3-dispositions.md."
36
41
  }
37
42
  }
38
43
  }
@@ -886,6 +886,7 @@ def lint_command(path: Path, text: str) -> LintResult:
886
886
  "error", "shim_missing_warning",
887
887
  "Deprecation shim must contain a one-line warning matching "
888
888
  "'⚠️ /<old-name> is deprecated; use /<cluster> <sub> instead.'"
889
+ " (or '/<cluster> --<flag>' for flag-clusters)"
889
890
  " (see docs/contracts/command-clusters.md § Deprecation shim contract)"
890
891
  ))
891
892
 
@@ -136,8 +136,31 @@ def _append_unknown(body: str, user_flat: dict[str, object], known: set[str]) ->
136
136
 
137
137
 
138
138
  def render_target(template_body: str, user_data: dict) -> str:
139
- """Return the desired `.agent-settings.yml` body for the given user data."""
140
- user_flat = _flatten(user_data) if user_data else {}
139
+ """Return the desired `.agent-settings.yml` body for the given user data.
140
+
141
+ The trailing ``_user:`` block (emitted by :func:`_append_unknown`) is
142
+ already in dotted-key form on every read after the first sync. Re-
143
+ flattening it would prepend another ``_user.`` segment on every run
144
+ and accumulate forever, so we strip the wrapper and merge its
145
+ contents straight into the flat dict.
146
+ """
147
+ if user_data:
148
+ user_only = user_data.pop("_user", None) if isinstance(user_data, dict) else None
149
+ user_flat = _flatten(user_data)
150
+ if isinstance(user_only, dict):
151
+ for key, value in user_only.items():
152
+ # Dotted keys round-trip verbatim — never re-flatten them.
153
+ if isinstance(key, str):
154
+ # Heal legacy corruption: pre-fix syncs prepended a
155
+ # `_user.` segment per run, so a key may carry an
156
+ # arbitrary number of them. Strip them all back to
157
+ # the original leaf path.
158
+ healed = key
159
+ while healed.startswith("_user."):
160
+ healed = healed[len("_user."):]
161
+ user_flat[healed] = value
162
+ else:
163
+ user_flat = {}
141
164
  known = _template_keys(template_body)
142
165
  body = _apply_user_values(template_body, user_flat)
143
166
  return _append_unknown(body, user_flat, known)
@@ -37,6 +37,13 @@ def count(kind: str) -> int:
37
37
  if not pdir.exists():
38
38
  return 0
39
39
  return sum(1 for f in pdir.glob("*.md") if f.name != "README.md")
40
+ if kind == "commands":
41
+ # Commands may be flat (`commands/<name>.md`) or nested under a
42
+ # cluster directory (`commands/<cluster>/<sub>.md`). Walk the tree
43
+ # and skip the AGENTS.md reference orchestrator.
44
+ return sum(
45
+ 1 for f in (SRC / kind).rglob("*.md") if f.name != "AGENTS.md"
46
+ )
40
47
  return sum(1 for _ in (SRC / kind).glob("*.md"))
41
48
 
42
49