@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
@@ -101,6 +101,14 @@ EXAMPLE_PATH_PATTERNS = [
101
101
  re.compile(r"skills/[\w-]+/SKILL\.md"), # example skill paths in commands
102
102
  re.compile(r"\{"), # template placeholders like {module}
103
103
  re.compile(r"\.compression-hashes\.json"), # JSON file, not .md
104
+ # Forward references inside in-flight planning docs (road-to-
105
+ # structural-optimization.md and its companion spike protocols).
106
+ # Each pattern below is removed once the matching phase lands.
107
+ re.compile(r"structural-optimization-3a-spike\.md"), # 3a.0.2
108
+ re.compile(r"contexts/judges/no-consolidate-rationale"), # 3a.0.2 abort
109
+ re.compile(r"contexts/judges/judge-shared-procedure"), # 3a.1
110
+ re.compile(r"contexts/analysis/project-analysis-core-procedure"), # 3b.1
111
+ re.compile(r"agents/roadmaps/phase6-non-overlap-evidence"), # 6.1 conditional
104
112
  ]
105
113
 
106
114
 
@@ -118,8 +126,20 @@ def collect_artifacts(root: Path) -> dict[str, set[str]]:
118
126
  arts["skills"].add(d.name)
119
127
  for f in (augment / "rules").glob("*.md") if (augment / "rules").exists() else []:
120
128
  arts["rules"].add(f.stem)
121
- for f in (augment / "commands").glob("*.md") if (augment / "commands").exists() else []:
122
- arts["commands"].add(f.stem)
129
+ cmd_dir = augment / "commands"
130
+ if cmd_dir.exists():
131
+ for f in cmd_dir.rglob("*.md"):
132
+ if f.name == "AGENTS.md":
133
+ continue
134
+ # Top-level: bare stem ("commit"). Nested: cluster-sub ("council-default")
135
+ # AND the cluster:sub form, since references may use either.
136
+ rel = f.relative_to(cmd_dir).with_suffix("")
137
+ parts = rel.parts
138
+ if len(parts) == 1:
139
+ arts["commands"].add(parts[0])
140
+ else:
141
+ arts["commands"].add("-".join(parts))
142
+ arts["commands"].add(":".join(parts))
123
143
  gdir = augment / "guidelines"
124
144
  if gdir.exists():
125
145
  for f in gdir.rglob("*.md"):
@@ -225,6 +245,13 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
225
245
  if any(p.search(raw_ref) for p in EXAMPLE_PATH_PATTERNS):
226
246
  continue
227
247
 
248
+ # Skip references into directories already excluded from scanning
249
+ # (gitignored audit trails, archived roadmaps). Files there are
250
+ # not committed, so existence checks would always fail in CI.
251
+ if any(raw_ref.startswith(skip + "/") or raw_ref == skip
252
+ for skip in SKIP_DIRS):
253
+ continue
254
+
228
255
  resolved = False
229
256
  # Try raw ref as-is from root (covers .agent-src/..., agents/..., etc.)
230
257
  if (root / raw_ref).exists():
@@ -247,6 +274,12 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
247
274
  if (prefix / rel).exists():
248
275
  resolved = True
249
276
  break
277
+ # `agents/state/*.json` are runtime hook state files —
278
+ # gitignored, written by hooks at session/turn time, never
279
+ # committed. Prose references to them are descriptive, not
280
+ # checkable file paths.
281
+ if not resolved and raw_ref.startswith("agents/state/"):
282
+ resolved = True
250
283
  if not resolved:
251
284
  broken.append(BrokenRef(
252
285
  file=str(filepath), line=i, ref=m.group(1),
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Safety-floor exclusion linter (Phase 2A.0 of road-to-structural-optimization).
3
+
4
+ Per Q3=A locked decision (council Round 3, 2026-05-03), the four
5
+ safety-floor always-rules are out of scope for Phase 2A slimming:
6
+
7
+ - non-destructive-by-default
8
+ - commit-policy
9
+ - scope-control
10
+ - verify-before-complete
11
+
12
+ This linter compares HEAD against a baseline ref (default: ``main``)
13
+ and fails CI if any of those four rule files were modified by the
14
+ working branch.
15
+
16
+ Lift via the two-gate rollback documented in
17
+ ``agents/roadmaps/road-to-structural-optimization.md`` § Phase 2A
18
+ Abort/rollback.
19
+
20
+ Exit codes: 0 = clean (or skipped — see ``--skip-if-no-baseline``),
21
+ 1 = safety-floor file modified, 3 = internal error.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ REPO_ROOT = Path(__file__).resolve().parent.parent
31
+ RULES_DIR_REL = ".agent-src.uncompressed/rules"
32
+ SAFETY_FLOOR = (
33
+ "non-destructive-by-default.md",
34
+ "commit-policy.md",
35
+ "scope-control.md",
36
+ "verify-before-complete.md",
37
+ )
38
+
39
+
40
+ def _run_git(args: list[str]) -> tuple[int, str]:
41
+ proc = subprocess.run(
42
+ ["git", *args],
43
+ cwd=REPO_ROOT,
44
+ capture_output=True,
45
+ text=True,
46
+ check=False,
47
+ )
48
+ return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
49
+
50
+
51
+ def _baseline_exists(ref: str) -> bool:
52
+ code, _ = _run_git(["rev-parse", "--verify", "--quiet", ref])
53
+ return code == 0
54
+
55
+
56
+ def _changed_files(baseline: str) -> list[str]:
57
+ code, output = _run_git(["diff", "--name-only", f"{baseline}...HEAD"])
58
+ if code != 0:
59
+ raise RuntimeError(f"git diff failed: {output}")
60
+ return [line.strip() for line in output.splitlines() if line.strip()]
61
+
62
+
63
+ def main() -> int:
64
+ parser = argparse.ArgumentParser(description=__doc__)
65
+ parser.add_argument(
66
+ "--baseline",
67
+ default="origin/main",
68
+ help="Baseline ref (default: origin/main)",
69
+ )
70
+ parser.add_argument(
71
+ "--skip-if-no-baseline",
72
+ action="store_true",
73
+ help="Exit 0 silently if baseline ref does not exist (local dev)",
74
+ )
75
+ args = parser.parse_args()
76
+
77
+ if not _baseline_exists(args.baseline):
78
+ if args.skip_if_no_baseline:
79
+ print(f"ℹ️ baseline {args.baseline} not found — skipped")
80
+ return 0
81
+ # Fallback: try plain `main`
82
+ if _baseline_exists("main"):
83
+ args.baseline = "main"
84
+ else:
85
+ print(
86
+ f"❌ baseline {args.baseline} (and `main`) not found. "
87
+ "Pass --skip-if-no-baseline to silence in local dev.",
88
+ file=sys.stderr,
89
+ )
90
+ return 3
91
+
92
+ try:
93
+ changed = _changed_files(args.baseline)
94
+ except RuntimeError as exc:
95
+ print(f"❌ {exc}", file=sys.stderr)
96
+ return 3
97
+
98
+ floor_paths = {f"{RULES_DIR_REL}/{name}" for name in SAFETY_FLOOR}
99
+ breaches = sorted(p for p in changed if p in floor_paths)
100
+
101
+ if breaches:
102
+ print(
103
+ "❌ Safety-floor rule(s) modified — Phase 2A is not allowed to "
104
+ "touch these (Q3=A locked decision):",
105
+ file=sys.stderr,
106
+ )
107
+ for path in breaches:
108
+ print(f" {path}", file=sys.stderr)
109
+ print(
110
+ "\n Lift via the two-gate rollback documented in "
111
+ "agents/roadmaps/road-to-structural-optimization.md "
112
+ "§ Phase 2A Abort/rollback.",
113
+ file=sys.stderr,
114
+ )
115
+ return 1
116
+
117
+ print(
118
+ f"✅ Safety-floor untouched ({len(SAFETY_FLOOR)} rules guarded "
119
+ f"vs. {args.baseline})."
120
+ )
121
+ return 0
122
+
123
+
124
+ if __name__ == "__main__":
125
+ sys.exit(main())
@@ -26,7 +26,10 @@ def load_commands(commands_dir: Path) -> list[CommandSpec]:
26
26
  this loader.
27
27
  """
28
28
  specs: list[CommandSpec] = []
29
- for path in sorted(commands_dir.glob("*.md")):
29
+ for path in sorted(commands_dir.rglob("*.md")):
30
+ # Skip cluster authoring docs — not commands.
31
+ if path.name == "AGENTS.md":
32
+ continue
30
33
  text = path.read_text(encoding="utf-8")
31
34
  data, _offset = parse_frontmatter(text)
32
35
  if data is None:
@@ -312,6 +312,29 @@ def generate_gemini_md() -> None:
312
312
  print(" ✅ Created GEMINI.md → AGENTS.md symlink")
313
313
 
314
314
 
315
+ def _command_slug(source_file: Path) -> str:
316
+ """Return the flat .claude/skills/ slug for a command source file.
317
+
318
+ Top-level commands keep their stem (`commit.md` → `commit`). Nested
319
+ commands flatten the relative path with `-` (`council/default.md` →
320
+ `council-default`). Keeps slug collisions out of `.claude/skills/`
321
+ while preserving native nested invocation in `.agent-src/commands/`.
322
+ """
323
+ rel = source_file.relative_to(COMMANDS_SOURCE)
324
+ return "-".join(rel.with_suffix("").parts)
325
+
326
+
327
+ def _iter_commands():
328
+ """Yield (source_file, slug) for every command .md file (recursive)."""
329
+ if not COMMANDS_SOURCE.exists():
330
+ return
331
+ for source_file in sorted(COMMANDS_SOURCE.rglob("*.md")):
332
+ # Skip the cluster AGENTS.md authoring doc (not a command).
333
+ if source_file.name == "AGENTS.md":
334
+ continue
335
+ yield source_file, _command_slug(source_file)
336
+
337
+
315
338
  def generate_claude_skills() -> None:
316
339
  """Create .claude/skills/ symlinks for ALL skills in .agent-src/skills/.
317
340
  """
@@ -321,16 +344,14 @@ def generate_claude_skills() -> None:
321
344
 
322
345
  # All skill directories in .agent-src/skills/
323
346
  skills = sorted([d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()])
324
- # All command names (to protect from stale cleanup)
325
- command_names = set()
326
- if COMMANDS_SOURCE.exists():
327
- command_names = {f.stem for f in COMMANDS_SOURCE.glob("*.md")}
347
+ # All command slugs (to protect from stale cleanup)
348
+ command_slugs = {slug for _, slug in _iter_commands()}
328
349
 
329
350
  CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
330
351
 
331
352
  # Clean stale symlinks (but not converted commands or README)
332
353
  for item in CLAUDE_SKILLS_DIR.iterdir():
333
- if item.is_symlink() and item.name not in skills and item.name not in command_names and item.name != "README.md":
354
+ if item.is_symlink() and item.name not in skills and item.name not in command_slugs and item.name != "README.md":
334
355
  item.unlink()
335
356
 
336
357
  count = 0
@@ -357,11 +378,15 @@ def extract_description_from_md(content: str) -> str:
357
378
 
358
379
 
359
380
  def generate_claude_commands() -> None:
360
- """Create .claude/skills/{name}/SKILL.md symlinks for ALL Augment commands.
381
+ """Create .claude/skills/{slug}/SKILL.md symlinks for ALL Augment commands.
361
382
 
362
383
  Commands in .agent-src/commands/ are the single source of truth.
363
384
  They must include name: and disable-model-invocation: true in frontmatter
364
385
  (added once, then maintained as part of the command file).
386
+
387
+ Top-level commands use their filename stem as the slug. Nested
388
+ cluster commands (e.g. `commands/council/default.md`) are flattened
389
+ to `council-default` so directories never collide in `.claude/skills/`.
365
390
  """
366
391
  if not COMMANDS_SOURCE.exists():
367
392
  print(" ⚠️ .agent-src/commands/ not found — skipping commands")
@@ -374,32 +399,53 @@ def generate_claude_commands() -> None:
374
399
  if SKILLS_SOURCE.exists():
375
400
  skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
376
401
 
402
+ # Track current command slugs for stale-directory cleanup
403
+ current_slugs: set[str] = set()
377
404
  count = 0
378
405
  skipped = 0
379
- for source_file in sorted(COMMANDS_SOURCE.glob("*.md")):
380
- name = source_file.stem
381
-
406
+ for source_file, slug in _iter_commands():
382
407
  # Skip if a real skill with the same name exists — skill takes priority
383
- if name in skill_names:
408
+ if slug in skill_names:
384
409
  skipped += 1
385
410
  continue
386
411
 
412
+ current_slugs.add(slug)
413
+
387
414
  # Create skill directory (real dir, symlinked SKILL.md inside)
388
- skill_dir = CLAUDE_SKILLS_DIR / name
415
+ skill_dir = CLAUDE_SKILLS_DIR / slug
389
416
  skill_dir.mkdir(parents=True, exist_ok=True)
390
417
 
391
418
  skill_file = skill_dir / "SKILL.md"
392
419
  if skill_file.exists() or skill_file.is_symlink():
393
420
  skill_file.unlink()
394
421
 
395
- # Symlink: .claude/skills/{name}/SKILL.md → ../../../.agent-src/commands/{name}.md
396
- rel_target = Path("../../../.agent-src/commands") / source_file.name
422
+ # Symlink: .claude/skills/{slug}/SKILL.md → ../../../.agent-src/commands/<rel-path>
423
+ rel_path = source_file.relative_to(COMMANDS_SOURCE)
424
+ rel_target = Path("../../../.agent-src/commands") / rel_path
397
425
  skill_file.symlink_to(rel_target)
398
426
  count += 1
399
427
 
428
+ # Clean stale command skill directories — real dirs from removed commands.
429
+ # Only delete if the directory contains exactly the SKILL.md symlink we created.
430
+ removed_dirs = 0
431
+ for item in CLAUDE_SKILLS_DIR.iterdir():
432
+ if not item.is_dir() or item.is_symlink():
433
+ continue
434
+ if item.name in skill_names or item.name in current_slugs:
435
+ continue
436
+ skill_md = item / "SKILL.md"
437
+ if skill_md.is_symlink():
438
+ entries = list(item.iterdir())
439
+ if len(entries) == 1 and entries[0].name == "SKILL.md":
440
+ skill_md.unlink()
441
+ item.rmdir()
442
+ removed_dirs += 1
443
+
400
444
  msg = f" ✅ Created {count} command symlinks in .claude/skills/"
401
445
  if skipped:
402
446
  msg += f" ({skipped} skipped — same-name skill exists)"
447
+ if removed_dirs:
448
+ msg += f" ({removed_dirs} stale dirs removed)"
403
449
  print(msg)
404
450
 
405
451
 
@@ -515,8 +561,11 @@ def project_to_augment() -> None:
515
561
  dst.symlink_to(Path("..") / ".agent-src" / name)
516
562
  print(f" ✅ Symlinked .augment/{name} → ../.agent-src/{name}")
517
563
 
518
- # Cleanup: remove any stray top-level entries in .augment/ that are no longer projected
519
- known = set(AUGMENT_SYMLINK_DIRS) | set(AUGMENT_SYMLINK_FILES) | {"rules"}
564
+ # Cleanup: remove any stray top-level entries in .augment/ that are no longer projected.
565
+ # `state` holds runtime state files written by hooks (onboarding-gate,
566
+ # context-hygiene, …) and must survive sync — it is regenerated by
567
+ # the next hook fire, not by compress.
568
+ known = set(AUGMENT_SYMLINK_DIRS) | set(AUGMENT_SYMLINK_FILES) | {"rules", "state"}
520
569
  for item in AUGMENT_DIR.iterdir():
521
570
  if item.name in known:
522
571
  continue
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ """Platform-agnostic PostToolUse hook for the `context-hygiene` rule.
3
+
4
+ Maintains a deterministic state file the rule body cites for the
5
+ freshness threshold, the 3-failure stop, and tool-loop detection. The
6
+ agent's job shrinks from "remember three counters" to "read this file
7
+ before responding".
8
+
9
+ Output: `agents/state/context-hygiene.json`
10
+ {
11
+ "tool_calls": <int>, // running PostToolUse count
12
+ "consecutive_same_tool": <int>, // includes the latest call
13
+ "last_tool": "<name>",
14
+ "tool_history": [..., last 5 names],
15
+ "loop_detected": <bool>, // ≥ 3 same tool in a row
16
+ "freshness_threshold": <int|null>, // 20/40/60 milestone hit
17
+ "checked_at": "<iso8601>"
18
+ }
19
+
20
+ Exit code is always 0.
21
+
22
+ CLI:
23
+ python3 scripts/context_hygiene_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 sys
31
+ from pathlib import Path
32
+
33
+ STATE_DIR = Path("agents") / "state"
34
+ STATE_FILE = STATE_DIR / "context-hygiene.json"
35
+
36
+ LOOP_THRESHOLD = 3 # 3+ consecutive same-tool calls
37
+ HISTORY_DEPTH = 5
38
+ FRESHNESS_MILESTONES = (20, 40, 60)
39
+
40
+
41
+ def _load_state(target: Path) -> dict:
42
+ if not target.is_file():
43
+ return {
44
+ "tool_calls": 0,
45
+ "consecutive_same_tool": 0,
46
+ "last_tool": None,
47
+ "tool_history": [],
48
+ "loop_detected": False,
49
+ "freshness_threshold": None,
50
+ }
51
+ try:
52
+ decoded = json.loads(target.read_text(encoding="utf-8"))
53
+ if isinstance(decoded, dict):
54
+ return decoded
55
+ except (OSError, json.JSONDecodeError):
56
+ pass
57
+ # Corrupt — start fresh, never block.
58
+ return {
59
+ "tool_calls": 0,
60
+ "consecutive_same_tool": 0,
61
+ "last_tool": None,
62
+ "tool_history": [],
63
+ "loop_detected": False,
64
+ "freshness_threshold": None,
65
+ }
66
+
67
+
68
+ def _extract_tool(payload: dict) -> str | None:
69
+ for key in ("tool_name", "toolName", "tool"):
70
+ v = payload.get(key)
71
+ if isinstance(v, str) and v:
72
+ return v
73
+ return None
74
+
75
+
76
+ def _milestone_hit(prev: int, curr: int) -> int | None:
77
+ """Return the milestone crossed by going from `prev` to `curr`, else None."""
78
+ for ms in FRESHNESS_MILESTONES:
79
+ if prev < ms <= curr:
80
+ return ms
81
+ return None
82
+
83
+
84
+ def _update(state: dict, tool: str | None) -> dict:
85
+ if tool is None:
86
+ # Non-tool event (e.g. malformed payload) — still mark we ran.
87
+ state["checked_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat(
88
+ timespec="seconds")
89
+ return state
90
+
91
+ prev_count = int(state.get("tool_calls") or 0)
92
+ curr_count = prev_count + 1
93
+ state["tool_calls"] = curr_count
94
+
95
+ last = state.get("last_tool")
96
+ if last == tool:
97
+ state["consecutive_same_tool"] = int(
98
+ state.get("consecutive_same_tool") or 0) + 1
99
+ else:
100
+ state["consecutive_same_tool"] = 1
101
+ state["last_tool"] = tool
102
+
103
+ hist = state.get("tool_history") or []
104
+ if not isinstance(hist, list):
105
+ hist = []
106
+ hist.append(tool)
107
+ state["tool_history"] = hist[-HISTORY_DEPTH:]
108
+
109
+ state["loop_detected"] = (
110
+ state["consecutive_same_tool"] >= LOOP_THRESHOLD)
111
+
112
+ ms = _milestone_hit(prev_count, curr_count)
113
+ if ms is not None:
114
+ state["freshness_threshold"] = ms
115
+ state["checked_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat(
116
+ timespec="seconds")
117
+ return state
118
+
119
+
120
+ def _write_state(consumer_root: Path, state: dict) -> None:
121
+ state_dir = consumer_root / STATE_DIR
122
+ state_dir.mkdir(parents=True, exist_ok=True)
123
+ target = consumer_root / STATE_FILE
124
+ tmp = target.with_suffix(".json.tmp")
125
+ tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
126
+ tmp.replace(target)
127
+
128
+
129
+ def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
130
+ payload: dict = {}
131
+ if stdin_text.strip():
132
+ try:
133
+ decoded = json.loads(stdin_text)
134
+ if isinstance(decoded, dict):
135
+ payload = decoded
136
+ except json.JSONDecodeError:
137
+ pass # silent no-op, never block
138
+
139
+ target = consumer_root / STATE_FILE
140
+ state = _load_state(target)
141
+ state = _update(state, _extract_tool(payload))
142
+
143
+ try:
144
+ _write_state(consumer_root, state)
145
+ except OSError:
146
+ if verbose:
147
+ print("context-hygiene-hook: state write failed",
148
+ file=sys.stderr)
149
+ return 0
150
+
151
+ if verbose:
152
+ print(
153
+ f"context-hygiene-hook: tool_calls={state.get('tool_calls')} "
154
+ f"loop={state.get('loop_detected')} "
155
+ f"threshold={state.get('freshness_threshold')}",
156
+ file=sys.stderr,
157
+ )
158
+ return 0
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ parser = argparse.ArgumentParser(description=__doc__)
163
+ parser.add_argument("--platform", default="generic",
164
+ help="informational platform tag")
165
+ parser.add_argument("--verbose", action="store_true",
166
+ help="emit one stderr line per invocation")
167
+ args = parser.parse_args(argv)
168
+ return run(sys.stdin.read(), consumer_root=Path.cwd(),
169
+ verbose=args.verbose)
170
+
171
+
172
+ if __name__ == "__main__": # pragma: no cover
173
+ sys.exit(main())
@@ -95,7 +95,10 @@ def _collect_rules() -> list[Entry]:
95
95
 
96
96
  def _collect_commands() -> list[Entry]:
97
97
  out = []
98
- for cmd_md in sorted((SRC / "commands").glob("*.md")):
98
+ cmd_dir = SRC / "commands"
99
+ for cmd_md in sorted(cmd_dir.rglob("*.md")):
100
+ if cmd_md.name == "AGENTS.md":
101
+ continue
99
102
  fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
100
103
  is_shim = bool(fm.get("superseded_by"))
101
104
  extra = ""
@@ -103,12 +106,13 @@ def _collect_commands() -> list[Entry]:
103
106
  extra = f"shim → /{fm['superseded_by']}"
104
107
  elif fm.get("cluster"):
105
108
  extra = f"cluster: {fm['cluster']}"
109
+ rel = cmd_md.relative_to(cmd_dir)
106
110
  out.append(Entry(
107
111
  kind="shim" if is_shim else "command",
108
112
  name=fm.get("name") or cmd_md.stem,
109
113
  description=_truncate(fm.get("description", "")),
110
114
  extra=extra,
111
- path=f".agent-src.uncompressed/commands/{cmd_md.name}",
115
+ path=f".agent-src.uncompressed/commands/{rel}",
112
116
  ))
113
117
  return out
114
118