@event4u/agent-config 1.13.0 → 1.14.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 (252) hide show
  1. package/.agent-src/commands/agent-handoff.md +3 -0
  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 +5 -1
  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 +5 -0
  11. package/.agent-src/commands/chat-history-resume.md +5 -0
  12. package/.agent-src/commands/chat-history.md +5 -0
  13. package/.agent-src/commands/check-current-md.md +126 -0
  14. package/.agent-src/commands/commit-in-chunks.md +98 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +3 -0
  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 +3 -0
  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 +24 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +3 -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 +4 -1
  64. package/.agent-src/commands/review-changes.md +4 -0
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +7 -0
  67. package/.agent-src/commands/roadmap-execute.md +12 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +3 -0
  70. package/.agent-src/commands/sync-agent-settings.md +3 -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 +4 -0
  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 +8 -0
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history.md +147 -118
  91. package/.agent-src/rules/cli-output-handling.md +26 -3
  92. package/.agent-src/rules/command-suggestion.md +133 -0
  93. package/.agent-src/rules/commit-policy.md +99 -0
  94. package/.agent-src/rules/direct-answers.md +114 -0
  95. package/.agent-src/rules/docs-sync.md +36 -0
  96. package/.agent-src/rules/downstream-changes.md +10 -9
  97. package/.agent-src/rules/improve-before-implement.md +9 -6
  98. package/.agent-src/rules/language-and-tone.md +81 -6
  99. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  100. package/.agent-src/rules/package-ci-checks.md +4 -0
  101. package/.agent-src/rules/preservation-guard.md +20 -0
  102. package/.agent-src/rules/roadmap-progress-sync.md +103 -30
  103. package/.agent-src/rules/scope-control.md +42 -1
  104. package/.agent-src/rules/size-enforcement.md +1 -3
  105. package/.agent-src/rules/skill-quality.md +3 -8
  106. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  107. package/.agent-src/rules/user-interaction.md +82 -50
  108. package/.agent-src/scripts/update_roadmap_progress.py +17 -5
  109. package/.agent-src/skills/blade-ui/SKILL.md +30 -5
  110. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  111. package/.agent-src/skills/command-writing/SKILL.md +41 -2
  112. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  113. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  114. package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
  115. package/.agent-src/skills/fe-design/SKILL.md +72 -60
  116. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  117. package/.agent-src/skills/flux/SKILL.md +31 -4
  118. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  119. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  120. package/.agent-src/skills/livewire/SKILL.md +30 -4
  121. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  122. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  123. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  124. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  125. package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
  126. package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
  127. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  128. package/.agent-src/skills/skill-writing/SKILL.md +1 -3
  129. package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
  130. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  131. package/.agent-src/templates/AGENTS.md +24 -6
  132. package/.agent-src/templates/agent-settings.md +149 -0
  133. package/.agent-src/templates/roadmaps.md +8 -2
  134. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  135. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  136. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  137. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  138. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  139. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  140. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  141. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  142. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  143. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  144. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  145. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  146. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  147. package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
  148. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
  149. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  150. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  151. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  152. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
  153. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
  154. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
  155. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  156. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  159. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  160. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  161. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  162. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  163. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  164. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  165. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  176. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  177. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  178. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  179. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  180. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  181. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  182. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  183. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  184. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  185. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  194. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  195. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  196. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  197. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
  198. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  199. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  200. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  201. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  202. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  203. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  204. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  205. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  206. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  207. package/.claude-plugin/marketplace.json +105 -2
  208. package/AGENTS.md +36 -8
  209. package/CHANGELOG.md +534 -0
  210. package/README.md +125 -4
  211. package/config/agent-settings.template.yml +45 -0
  212. package/config/gitignore-block.txt +4 -0
  213. package/docs/architecture.md +28 -1
  214. package/docs/development.md +1 -1
  215. package/docs/getting-started.md +2 -2
  216. package/docs/installation.md +86 -0
  217. package/docs/showcase.md +204 -0
  218. package/package.json +1 -1
  219. package/scripts/agent-config +199 -0
  220. package/scripts/audit_cloud_compatibility.py +288 -0
  221. package/scripts/build_cloud_bundle.py +458 -0
  222. package/scripts/build_linear_digest.py +263 -0
  223. package/scripts/chat_history.py +796 -7
  224. package/scripts/check_compression.py +139 -0
  225. package/scripts/check_iron_law_prominence.py +143 -0
  226. package/scripts/check_md_language.py +159 -0
  227. package/scripts/check_portability.py +36 -0
  228. package/scripts/check_reply_consistency.py +140 -0
  229. package/scripts/command_suggester/__init__.py +51 -0
  230. package/scripts/command_suggester/cooldown.py +132 -0
  231. package/scripts/command_suggester/loader.py +70 -0
  232. package/scripts/command_suggester/match.py +180 -0
  233. package/scripts/command_suggester/rank.py +120 -0
  234. package/scripts/command_suggester/render.py +86 -0
  235. package/scripts/command_suggester/sanitize.py +113 -0
  236. package/scripts/command_suggester/settings.py +125 -0
  237. package/scripts/command_suggester/types.py +78 -0
  238. package/scripts/hooks/augment-chat-history.sh +56 -0
  239. package/scripts/install-hooks.sh +67 -0
  240. package/scripts/install.py +150 -33
  241. package/scripts/lint_marketplace.py +27 -0
  242. package/scripts/migrate_command_suggestions.py +151 -0
  243. package/scripts/schemas/command.schema.json +41 -0
  244. package/scripts/skill_linter.py +67 -0
  245. package/scripts/sync_agent_settings.py +42 -12
  246. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  247. package/templates/consumer-settings/claude-settings.json +55 -1
  248. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  249. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  250. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  251. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
  252. /package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +0 -0
@@ -8,6 +8,8 @@ Checks that compression preserved structural integrity:
8
8
  - All code blocks preserved exactly
9
9
  - YAML frontmatter identical
10
10
  - Word count reduction within healthy range (10-60%)
11
+ - Iron Law sections (## Iron Law / ### Iron Law / ## The Iron Law / Iron Laws / numbered)
12
+ preserved per `preservation-guard`: heading verbatim at original level, ≤ 15% reduction
11
13
 
12
14
  Exit codes: 0 = clean, 1 = issues found, 3 = internal error
13
15
  """
@@ -60,6 +62,103 @@ def extract_frontmatter(text: str) -> str:
60
62
  return m.group(1).strip() if m else ""
61
63
 
62
64
 
65
+ # Matches `## Iron Law`, `## The Iron Law`, `## Iron Laws`, `### Iron Law — …`,
66
+ # `## Iron Law 1 — …`, etc. Any heading level 2-6.
67
+ IRON_LAW_HEADING = re.compile(r"^(#{2,6})\s+(The\s+)?Iron Laws?\b")
68
+
69
+ LIST_ITEM_RE = re.compile(r"^(?:[-*+]|\d+\.)\s")
70
+ INNER_HEADING_RE = re.compile(r"^#{1,6}\s")
71
+
72
+
73
+ def count_iron_law_structure(body: str) -> dict:
74
+ """Count structural units in an Iron Law body.
75
+
76
+ Returns counts of paragraphs (blank-line-separated prose blocks),
77
+ list items (bullet + numbered), and fenced code blocks. Caveman
78
+ compression may shorten word count freely; what must NOT change is
79
+ the count of these structural units. Each represents a passage of
80
+ the law that the source decided to keep.
81
+
82
+ Multi-line list items (bullet text wrapped to indented continuation
83
+ lines, no blank line between) count as ONE list item, not as a
84
+ list item plus a paragraph.
85
+ """
86
+ paragraphs = 0
87
+ list_items = 0
88
+ code_blocks = 0
89
+ in_code = False
90
+ state = "blank" # "blank" | "paragraph" | "list"
91
+ for line in body.splitlines():
92
+ stripped = line.strip()
93
+ if stripped.startswith("```"):
94
+ if not in_code:
95
+ code_blocks += 1
96
+ in_code = not in_code
97
+ state = "blank"
98
+ continue
99
+ if in_code:
100
+ continue
101
+ if not stripped:
102
+ state = "blank"
103
+ continue
104
+ if LIST_ITEM_RE.match(stripped):
105
+ list_items += 1
106
+ state = "list"
107
+ continue
108
+ if INNER_HEADING_RE.match(stripped):
109
+ state = "blank"
110
+ continue
111
+ # Indented non-empty line right after a list item is a wrap
112
+ # continuation of that item, not a new paragraph.
113
+ if state == "list" and line.startswith((" ", "\t")):
114
+ continue
115
+ if state != "paragraph":
116
+ paragraphs += 1
117
+ state = "paragraph"
118
+ return {"paragraphs": paragraphs, "list_items": list_items, "code_blocks": code_blocks}
119
+
120
+
121
+ def extract_iron_law_sections(text: str) -> list[tuple[str, int, str]]:
122
+ """Return [(heading, level, body)] for each Iron Law section.
123
+
124
+ Body is everything after the heading until the next heading at the same
125
+ or higher (numerically lower) level — fenced code blocks included verbatim.
126
+ """
127
+ lines = text.splitlines()
128
+ sections: list[tuple[str, int, str]] = []
129
+ i = 0
130
+ in_code = False
131
+ while i < len(lines):
132
+ line = lines[i]
133
+ if line.strip().startswith("```"):
134
+ in_code = not in_code
135
+ i += 1
136
+ continue
137
+ if not in_code:
138
+ m = IRON_LAW_HEADING.match(line)
139
+ if m:
140
+ heading = line.rstrip()
141
+ level = len(m.group(1))
142
+ body_lines: list[str] = []
143
+ j = i + 1
144
+ inner_code = False
145
+ while j < len(lines):
146
+ jline = lines[j]
147
+ if jline.strip().startswith("```"):
148
+ inner_code = not inner_code
149
+ if not inner_code:
150
+ hm = re.match(r"^(#{1,6})\s", jline)
151
+ if hm and len(hm.group(1)) <= level:
152
+ break
153
+ body_lines.append(jline)
154
+ j += 1
155
+ sections.append((heading, level, "\n".join(body_lines)))
156
+ i = j
157
+ continue
158
+ i += 1
159
+ return sections
160
+
161
+
63
162
  def check_pair(rel_path: str, source: str, compressed: str) -> List[Issue]:
64
163
  """Compare source and compressed versions of a file."""
65
164
  issues: List[Issue] = []
@@ -96,6 +195,46 @@ def check_pair(rel_path: str, source: str, compressed: str) -> List[Issue]:
96
195
  issues.append(Issue(rel_path, "modified_code_block", "error",
97
196
  f"Code block {i+1} content changed during compression"))
98
197
 
198
+ # Iron Law preservation — non-negotiable behavioral rules, see preservation-guard
199
+ src_laws = extract_iron_law_sections(source)
200
+ cmp_laws = extract_iron_law_sections(compressed)
201
+ cmp_law_map = {h: (lvl, body) for h, lvl, body in cmp_laws}
202
+ # Build a level-agnostic lookup so we can detect heading-level downgrades
203
+ # (`## Iron Law` → `### Iron Law`).
204
+ cmp_law_by_text = {h.lstrip("# ").strip(): (lvl, h, body)
205
+ for h, lvl, body in cmp_laws}
206
+ for src_heading, src_level, src_body in src_laws:
207
+ src_text = src_heading.lstrip("# ").strip()
208
+ if src_heading not in cmp_law_map:
209
+ # Heading text may exist at a different level → downgrade
210
+ if src_text in cmp_law_by_text:
211
+ cmp_level, cmp_heading, _ = cmp_law_by_text[src_text]
212
+ if cmp_level != src_level:
213
+ issues.append(Issue(rel_path, "iron_law_heading_downgrade", "error",
214
+ f"Iron Law heading level changed: "
215
+ f"{'#' * src_level} → {'#' * cmp_level} "
216
+ f"({src_heading.strip()})"))
217
+ continue
218
+ issues.append(Issue(rel_path, "iron_law_missing", "error",
219
+ f"Iron Law section removed during compression: "
220
+ f"{src_heading.strip()}"))
221
+ continue
222
+ # Section exists at correct level — check structural-unit survival.
223
+ # Caveman compression is fine (drop articles, terse phrasing); what
224
+ # must NOT change is the count of paragraphs, list items, and code
225
+ # blocks. Each is a passage the source kept on purpose.
226
+ _, cmp_body = cmp_law_map[src_heading]
227
+ src_struct = count_iron_law_structure(src_body)
228
+ cmp_struct = count_iron_law_structure(cmp_body)
229
+ for kind, src_n in src_struct.items():
230
+ cmp_n = cmp_struct[kind]
231
+ if cmp_n < src_n:
232
+ issues.append(Issue(rel_path, "iron_law_passage_dropped", "error",
233
+ f"Iron Law section dropped "
234
+ f"{src_n - cmp_n} {kind} "
235
+ f"({src_n} → {cmp_n}): "
236
+ f"{src_heading.strip()}"))
237
+
99
238
  # Word count ratio
100
239
  src_words = len(source.split())
101
240
  cmp_words = len(compressed.split())
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Iron Law prominence checker — enforces that any rule file declaring an
4
+ "Iron Law" places it at the top of the file at H2 level.
5
+
6
+ Rules:
7
+ 1. No heading at H3 or deeper may match "Iron Law(s)" — Iron Laws must
8
+ be H2 sections, never sub-sections.
9
+ 2. If a file declares one or more Iron-Law H2 sections, at least one
10
+ of them must be among the first two H2 headings of the file.
11
+
12
+ Files with no Iron-Law heading at all are exempt — they may legitimately
13
+ reference Iron Laws from other rules in prose only.
14
+
15
+ Code blocks are skipped to avoid false positives on quoted text.
16
+
17
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import re
25
+ import sys
26
+ from dataclasses import dataclass, asdict
27
+ from pathlib import Path
28
+
29
+ HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
30
+ IRON_LAW_RE = re.compile(r"\biron\s+laws?\b", re.IGNORECASE)
31
+ FENCE_RE = re.compile(r"^\s*```")
32
+
33
+
34
+ @dataclass
35
+ class Violation:
36
+ file: str
37
+ line: int
38
+ kind: str # "deep_iron_law" | "buried_iron_law"
39
+ detail: str
40
+
41
+
42
+ def _parse_headings(text: str) -> list[tuple[int, int, str]]:
43
+ """Return (line_no, depth, title) for each heading outside code fences."""
44
+ headings: list[tuple[int, int, str]] = []
45
+ in_fence = False
46
+ for lineno, raw in enumerate(text.splitlines(), start=1):
47
+ if FENCE_RE.match(raw):
48
+ in_fence = not in_fence
49
+ continue
50
+ if in_fence:
51
+ continue
52
+ m = HEADING_RE.match(raw)
53
+ if not m:
54
+ continue
55
+ depth = len(m.group(1))
56
+ title = m.group(2).strip()
57
+ headings.append((lineno, depth, title))
58
+ return headings
59
+
60
+
61
+ def scan_file(path: Path) -> list[Violation]:
62
+ text = path.read_text(encoding="utf-8")
63
+ headings = _parse_headings(text)
64
+
65
+ violations: list[Violation] = []
66
+
67
+ # Rule 1: no Iron Law at H3 or deeper
68
+ for lineno, depth, title in headings:
69
+ if depth >= 3 and IRON_LAW_RE.search(title):
70
+ violations.append(Violation(
71
+ file=str(path), line=lineno, kind="deep_iron_law",
72
+ detail=f"H{depth} heading `{title}` — promote to H2",
73
+ ))
74
+
75
+ # Rule 2: if any H2 Iron Law exists, it must be in first 2 H2 positions
76
+ h2 = [(ln, t) for ln, d, t in headings if d == 2]
77
+ iron_h2 = [(ln, t) for ln, t in h2 if IRON_LAW_RE.search(t)]
78
+ if iron_h2:
79
+ first_two_lines = {ln for ln, _ in h2[:2]}
80
+ if not any(ln in first_two_lines for ln, _ in iron_h2):
81
+ first_iron_ln, first_iron_title = iron_h2[0]
82
+ preceding = [t for ln, t in h2 if ln < first_iron_ln]
83
+ violations.append(Violation(
84
+ file=str(path), line=first_iron_ln, kind="buried_iron_law",
85
+ detail=(
86
+ f"Iron Law H2 `{first_iron_title}` at line {first_iron_ln} "
87
+ f"is preceded by {len(preceding)} non-Iron-Law H2 section(s): "
88
+ f"{preceding}. Move Iron Law into the first 2 H2 positions."
89
+ ),
90
+ ))
91
+
92
+ return violations
93
+
94
+
95
+ def _resolve_targets(paths: list[str]) -> list[Path]:
96
+ out: list[Path] = []
97
+ for raw in paths:
98
+ p = Path(raw)
99
+ if p.is_dir():
100
+ out.extend(sorted(p.rglob("*.md")))
101
+ elif p.suffix == ".md":
102
+ out.append(p)
103
+ return out
104
+
105
+
106
+ def main() -> int:
107
+ parser = argparse.ArgumentParser(description=__doc__)
108
+ parser.add_argument(
109
+ "paths", nargs="*",
110
+ default=[".agent-src.uncompressed/rules"],
111
+ help="Files or directories to scan (default: .agent-src.uncompressed/rules)",
112
+ )
113
+ parser.add_argument("--format", choices=["text", "json"], default="text")
114
+ args = parser.parse_args()
115
+
116
+ targets = _resolve_targets(args.paths)
117
+ all_violations: list[Violation] = []
118
+ for path in targets:
119
+ if not path.exists():
120
+ print(f"āš ļø Not found: {path}", file=sys.stderr)
121
+ continue
122
+ all_violations.extend(scan_file(path))
123
+
124
+ if args.format == "json":
125
+ print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
126
+ else:
127
+ if not all_violations:
128
+ print(f"āœ… Iron Law prominence clean ({len(targets)} file(s) scanned).")
129
+ else:
130
+ print(f"āŒ {len(all_violations)} Iron-Law prominence violation(s):\n")
131
+ for v in all_violations:
132
+ print(f" {v.file}:{v.line} — {v.kind}")
133
+ print(f" │ {v.detail}")
134
+
135
+ return 1 if all_violations else 0
136
+
137
+
138
+ if __name__ == "__main__":
139
+ try:
140
+ sys.exit(main())
141
+ except Exception as exc: # noqa: BLE001
142
+ print(f"āŒ Internal error: {exc}", file=sys.stderr)
143
+ sys.exit(3)
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Markdown language checker — enforces language-and-tone § ".md files are ALWAYS English".
4
+
5
+ Scans .md files for German content (umlauts, function words, quoted DE phrases)
6
+ in body prose, skipping:
7
+ - Fenced code blocks (``` ... ```)
8
+ - Inline code (`...`)
9
+ - Labeled DE: ... Ā· EN: ... anchor blocks
10
+ - URLs and file paths inside backticks
11
+
12
+ Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import re
20
+ import sys
21
+ from dataclasses import dataclass, asdict
22
+ from pathlib import Path
23
+
24
+ # Umlauts and German-only characters
25
+ UMLAUT_RE = re.compile(r"[Ć¤Ć¶Ć¼Ć„Ć–ĆœĆŸ]")
26
+
27
+ # German function words that almost never appear in English technical prose
28
+ DE_WORDS = [
29
+ "für", "nicht", "dass", "wenn", "sollte", "werden", "arbeite",
30
+ "selbststƤndig", "jetzt", "einfach", "weiter", "lƶsche", "frag",
31
+ "schreib", "mach", "auch", "hier", "diese", "dieser", "dieses",
32
+ "vermutlich", "bitte", "kannst", "sollen", "müssen", "wäre",
33
+ ]
34
+ DE_WORD_RE = re.compile(
35
+ r"\b(" + "|".join(re.escape(w) for w in DE_WORDS) + r")\b",
36
+ re.IGNORECASE,
37
+ )
38
+
39
+ # Labeled bilingual anchor: lines starting with "DE:" or "- DE:" (and same for EN)
40
+ DE_ANCHOR_RE = re.compile(r"^\s*[-*]?\s*(DE|EN):\s", re.IGNORECASE)
41
+
42
+ # Inline code spans
43
+ INLINE_CODE_RE = re.compile(r"`[^`]*`")
44
+
45
+ # Per-line escape: append `<!-- md-language-check: ignore -->` to a line
46
+ # to suppress findings on that line. For meta-documentation that quotes
47
+ # German tokens as trigger examples (e.g. inside language-and-tone.md).
48
+ IGNORE_RE = re.compile(r"<!--\s*md-language-check:\s*ignore\s*-->", re.IGNORECASE)
49
+
50
+
51
+ @dataclass
52
+ class Violation:
53
+ file: str
54
+ line: int
55
+ kind: str # "umlaut" | "de_word"
56
+ match: str
57
+ context: str
58
+
59
+
60
+ def _strip_inline_code(text: str) -> str:
61
+ return INLINE_CODE_RE.sub("", text)
62
+
63
+
64
+ def scan_file(path: Path) -> list[Violation]:
65
+ violations: list[Violation] = []
66
+ try:
67
+ lines = path.read_text(encoding="utf-8").splitlines()
68
+ except (OSError, UnicodeDecodeError) as exc:
69
+ print(f"āš ļø Cannot read {path}: {exc}", file=sys.stderr)
70
+ return violations
71
+
72
+ in_fence = False
73
+ in_frontmatter = False
74
+ for lineno, raw in enumerate(lines, start=1):
75
+ stripped = raw.lstrip()
76
+
77
+ # YAML frontmatter at top of file
78
+ if lineno == 1 and stripped == "---":
79
+ in_frontmatter = True
80
+ continue
81
+ if in_frontmatter:
82
+ if stripped == "---":
83
+ in_frontmatter = False
84
+ continue
85
+
86
+ # Fenced code blocks
87
+ if stripped.startswith("```") or stripped.startswith("~~~"):
88
+ in_fence = not in_fence
89
+ continue
90
+ if in_fence:
91
+ continue
92
+
93
+ # Indented code blocks (4+ leading spaces, non-list)
94
+ if raw.startswith(" ") and not stripped.startswith(("-", "*", "+", "1", "2", "3", "4", "5", "6", "7", "8", "9")):
95
+ continue
96
+
97
+ # Labeled bilingual anchor — allowed location for DE prose
98
+ if DE_ANCHOR_RE.match(raw):
99
+ continue
100
+
101
+ # Per-line opt-out marker
102
+ if IGNORE_RE.search(raw):
103
+ continue
104
+
105
+ # Strip inline code spans before scanning
106
+ scan_text = _strip_inline_code(raw)
107
+
108
+ for m in UMLAUT_RE.finditer(scan_text):
109
+ violations.append(Violation(
110
+ file=str(path), line=lineno, kind="umlaut",
111
+ match=m.group(0), context=raw.rstrip(),
112
+ ))
113
+
114
+ for m in DE_WORD_RE.finditer(scan_text):
115
+ violations.append(Violation(
116
+ file=str(path), line=lineno, kind="de_word",
117
+ match=m.group(0), context=raw.rstrip(),
118
+ ))
119
+
120
+ return violations
121
+
122
+
123
+ def main() -> int:
124
+ parser = argparse.ArgumentParser(description=__doc__)
125
+ parser.add_argument("paths", nargs="+", help="One or more .md files to scan")
126
+ parser.add_argument("--format", choices=["text", "json"], default="text")
127
+ args = parser.parse_args()
128
+
129
+ all_violations: list[Violation] = []
130
+ for raw_path in args.paths:
131
+ path = Path(raw_path)
132
+ if not path.exists():
133
+ print(f"āš ļø Not found: {path}", file=sys.stderr)
134
+ continue
135
+ if path.suffix != ".md":
136
+ print(f"āš ļø Skipping non-.md: {path}", file=sys.stderr)
137
+ continue
138
+ all_violations.extend(scan_file(path))
139
+
140
+ if args.format == "json":
141
+ print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
142
+ else:
143
+ if not all_violations:
144
+ print("āœ… No German content detected.")
145
+ else:
146
+ print(f"āŒ {len(all_violations)} violation(s) found:\n")
147
+ for v in all_violations:
148
+ print(f" {v.file}:{v.line} — {v.kind} `{v.match}`")
149
+ print(f" │ {v.context}")
150
+
151
+ return 1 if all_violations else 0
152
+
153
+
154
+ if __name__ == "__main__":
155
+ try:
156
+ sys.exit(main())
157
+ except Exception as exc: # noqa: BLE001
158
+ print(f"āŒ Internal error: {exc}", file=sys.stderr)
159
+ sys.exit(3)
@@ -329,6 +329,42 @@ _CLI_INVOCATION_MAP: list[tuple[re.Pattern, str]] = [
329
329
  re.compile(r"bash\s+scripts/first-run\.sh\b"),
330
330
  "./agent-config first-run",
331
331
  ),
332
+ (
333
+ re.compile(r"(?:PYTHONPATH=\S+\s+)?python3\s+-m\s+work_engine\b"),
334
+ "./agent-config implement-ticket",
335
+ ),
336
+ (
337
+ re.compile(r"(?:PYTHONPATH=\S+\s+)?python3\s+-m\s+implement_ticket\b"),
338
+ "./agent-config implement-ticket",
339
+ ),
340
+ (
341
+ re.compile(r"python3\s+scripts/memory_lookup\.py\b"),
342
+ "./agent-config memory:lookup",
343
+ ),
344
+ (
345
+ re.compile(r"python3\s+scripts/memory_signal\.py\b"),
346
+ "./agent-config memory:signal",
347
+ ),
348
+ (
349
+ re.compile(r"python3\s+scripts/memory_hash\.py\b"),
350
+ "./agent-config memory:hash",
351
+ ),
352
+ (
353
+ re.compile(r"python3\s+scripts/check_memory_proposal\.py\b"),
354
+ "./agent-config memory:check-proposal",
355
+ ),
356
+ (
357
+ re.compile(r"python3\s+scripts/check_memory\.py\b"),
358
+ "./agent-config memory:check",
359
+ ),
360
+ (
361
+ re.compile(r"python3\s+scripts/check_proposal\.py\b"),
362
+ "./agent-config proposal:check",
363
+ ),
364
+ (
365
+ re.compile(r"python3\s+scripts/refine_ticket_detect\.py\b"),
366
+ "./agent-config refine-ticket:detect",
367
+ ),
332
368
  ]
333
369
 
334
370
  # Paths that legitimately document the raw invocations (e.g. the CLI's
@@ -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
+ ]