@tenkara/afm-linux-arm64 0.0.0-stage → 4.0.0-alpha.6

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 (276) hide show
  1. package/bin/afm +4 -0
  2. package/content/.coupling-baseline.tsv +1 -0
  3. package/content/.pending-commands.txt +2 -0
  4. package/content/CHANGELOG-archive.md +806 -0
  5. package/content/CHANGELOG.md +4256 -0
  6. package/content/PRINCIPLES.md +121 -0
  7. package/content/README-light.md +160 -0
  8. package/content/README.md +152 -0
  9. package/content/code-style/_any/file-size.md +62 -0
  10. package/content/code-style/_any/imports.md +68 -0
  11. package/content/code-style/_any/naming.md +72 -0
  12. package/content/code-style/_any/test-placement.md +57 -0
  13. package/content/code-style/nextjs-app-router/imports.md +50 -0
  14. package/content/code-style/nextjs-app-router/naming.md +50 -0
  15. package/content/code-style/prisma-postgres/error-boundary.md +55 -0
  16. package/content/code-style/prisma-postgres/imports.md +61 -0
  17. package/content/code-style/shadcn-ui/naming.md +48 -0
  18. package/content/code-style/stripe-billing/naming.md +76 -0
  19. package/content/code-style/trigger-dev-v4/imports.md +58 -0
  20. package/content/code-style/trigger-dev-v4/naming.md +49 -0
  21. package/content/code-style/trigger-dev-v4/test-placement.md +61 -0
  22. package/content/code-style/trpc-v11/error-boundary.md +51 -0
  23. package/content/code-style/trpc-v11/file-size.md +44 -0
  24. package/content/code-style/trpc-v11/naming.md +60 -0
  25. package/content/code-style/trpc-v11/test-placement.md +52 -0
  26. package/content/core/ach.md +265 -0
  27. package/content/core/afm.md +496 -0
  28. package/content/core/gotchas.md +18 -0
  29. package/content/core/prd.md +143 -0
  30. package/content/core/protocol.md +160 -0
  31. package/content/core/ust.md +65 -0
  32. package/content/guardrails/README.md +55 -0
  33. package/content/guardrails/dsl-com-ordem-obrigatoria.md +47 -0
  34. package/content/guardrails/exaustividade-de-union.md +48 -0
  35. package/content/guardrails/fronteira-de-import.md +57 -0
  36. package/content/hooks/README.md +72 -0
  37. package/content/interview/bootstrap.md +415 -0
  38. package/content/interview/promotion.md +117 -0
  39. package/content/interview/refactor.md +381 -0
  40. package/content/interview/triggers.md +109 -0
  41. package/content/modules/nextjs-app-router/ach.patch.md +44 -0
  42. package/content/modules/nextjs-app-router/gotchas.seed.md +78 -0
  43. package/content/modules/nextjs-app-router/rules.patch.md +35 -0
  44. package/content/modules/prisma-postgres/ach.patch.md +65 -0
  45. package/content/modules/prisma-postgres/rules.patch.md +28 -0
  46. package/content/modules/shadcn-ui/ach.patch.md +42 -0
  47. package/content/modules/stripe-billing/ach.patch.md +46 -0
  48. package/content/modules/stripe-billing/gotchas.seed.md +73 -0
  49. package/content/modules/stripe-billing/rules.patch.md +30 -0
  50. package/content/modules/trigger-dev-v4/ach.patch.md +64 -0
  51. package/content/modules/trigger-dev-v4/gotchas.seed.md +53 -0
  52. package/content/modules/trigger-dev-v4/rules.patch.md +35 -0
  53. package/content/modules/trpc-v11/ach.patch.md +56 -0
  54. package/content/modules/trpc-v11/rules.patch.md +34 -0
  55. package/content/modules/vireya-ui/ach.patch.md +85 -0
  56. package/content/modules/vireya-ui/gotchas.seed.md +98 -0
  57. package/content/modules/vireya-ui/rules.patch.md +36 -0
  58. package/content/ops/adr.md +54 -0
  59. package/content/ops/analyze.md +129 -0
  60. package/content/ops/clarify.md +86 -0
  61. package/content/ops/deliver.md +211 -0
  62. package/content/ops/diagnose.md +146 -0
  63. package/content/ops/evolve.md +317 -0
  64. package/content/ops/export-template.md +97 -0
  65. package/content/ops/generalize.md +120 -0
  66. package/content/ops/gotcha.md +46 -0
  67. package/content/ops/plan.md +61 -0
  68. package/content/ops/policy.md +63 -0
  69. package/content/ops/promote.md +72 -0
  70. package/content/ops/protocol.md +51 -0
  71. package/content/ops/reconcile.md +44 -0
  72. package/content/ops/reflect.md +180 -0
  73. package/content/ops/remediate.md +146 -0
  74. package/content/ops/restructure.md +145 -0
  75. package/content/ops/rule.md +165 -0
  76. package/content/ops/specify.md +62 -0
  77. package/content/ops/tasks.md +67 -0
  78. package/content/ops/update.md +223 -0
  79. package/content/ops/verify.md +69 -0
  80. package/content/patterns/adapter.md +46 -0
  81. package/content/patterns/facade.md +47 -0
  82. package/content/patterns/factory.md +46 -0
  83. package/content/patterns/null-object.md +55 -0
  84. package/content/patterns/result-vs-exception.md +52 -0
  85. package/content/patterns/strategy.md +45 -0
  86. package/content/patterns/template-method-vs-composition.md +50 -0
  87. package/content/questions/adr.questions.yaml +14 -0
  88. package/content/questions/analyze.questions.yaml +12 -0
  89. package/content/questions/bootstrap-mapping.questions.yaml +14 -0
  90. package/content/questions/bootstrap-rules.questions.yaml +10 -0
  91. package/content/questions/bootstrap-stack.questions.yaml +23 -0
  92. package/content/questions/clarify.questions.yaml +10 -0
  93. package/content/questions/code-style.questions.yaml +12 -0
  94. package/content/questions/confirm.questions.yaml +12 -0
  95. package/content/questions/deliver.questions.yaml +23 -0
  96. package/content/questions/evolve.questions.yaml +15 -0
  97. package/content/questions/export-template-apply.questions.yaml +11 -0
  98. package/content/questions/export-template.questions.yaml +23 -0
  99. package/content/questions/gates.questions.yaml +27 -0
  100. package/content/questions/generalize.questions.yaml +13 -0
  101. package/content/questions/gotcha.questions.yaml +11 -0
  102. package/content/questions/init-template.questions.yaml +10 -0
  103. package/content/questions/init.questions.yaml +28 -0
  104. package/content/questions/plan.questions.yaml +14 -0
  105. package/content/questions/refactor.questions.yaml +23 -0
  106. package/content/questions/reflect.questions.yaml +13 -0
  107. package/content/questions/remediate.questions.yaml +27 -0
  108. package/content/questions/restructure.questions.yaml +15 -0
  109. package/content/questions/rf-mapping.questions.yaml +12 -0
  110. package/content/questions/rule-scope.questions.yaml +12 -0
  111. package/content/questions/rule.questions.yaml +12 -0
  112. package/content/questions/specify.questions.yaml +23 -0
  113. package/content/questions/update-artifacts.questions.yaml +10 -0
  114. package/content/questions/update-strings.questions.yaml +14 -0
  115. package/content/questions/update.questions.yaml +10 -0
  116. package/content/questions/verify.questions.yaml +9 -0
  117. package/content/roles/executor.md +18 -0
  118. package/content/roles/explorer.md +22 -0
  119. package/content/roles/reviewer.md +23 -0
  120. package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
  121. package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
  122. package/content/rubrics/error-classification.md +98 -0
  123. package/content/rubrics/failure-classification.md +113 -0
  124. package/content/rubrics/negative-filters.md +38 -0
  125. package/content/rubrics/solid-triggers.md +121 -0
  126. package/content/rubrics/template-vs-streaming-precedence.md +34 -0
  127. package/content/rubrics/validation-boundary.md +94 -0
  128. package/content/rubrics/when-to-create-dsl.md +65 -0
  129. package/content/rubrics/when-to-create-lib.md +49 -0
  130. package/content/rubrics/when-to-create-module.md +39 -0
  131. package/content/rubrics/when-to-evolve-methodology.md +62 -0
  132. package/content/skills/adr.md +42 -0
  133. package/content/skills/analyze.md +34 -0
  134. package/content/skills/clarify.md +34 -0
  135. package/content/skills/deliver.md +42 -0
  136. package/content/skills/diagnose.md +50 -0
  137. package/content/skills/evolve.md +67 -0
  138. package/content/skills/export-template.md +40 -0
  139. package/content/skills/generalize.md +47 -0
  140. package/content/skills/gotcha.md +40 -0
  141. package/content/skills/init.md +82 -0
  142. package/content/skills/plan.md +34 -0
  143. package/content/skills/policy.md +31 -0
  144. package/content/skills/promote.md +41 -0
  145. package/content/skills/protocol.md +42 -0
  146. package/content/skills/reconcile.md +49 -0
  147. package/content/skills/refactor.md +84 -0
  148. package/content/skills/reflect.md +53 -0
  149. package/content/skills/remediate.md +39 -0
  150. package/content/skills/restructure.md +52 -0
  151. package/content/skills/rule.md +44 -0
  152. package/content/skills/specify.md +34 -0
  153. package/content/skills/tasks.md +34 -0
  154. package/content/skills/update.md +56 -0
  155. package/content/skills/verify.md +31 -0
  156. package/content/templates/adr.template.md +51 -0
  157. package/content/templates/evolution-report.template.md +129 -0
  158. package/content/templates/feature-plan.template.md +86 -0
  159. package/content/templates/feature-research.template.md +57 -0
  160. package/content/templates/feature-spec.template.md +86 -0
  161. package/content/templates/feature-tasks.induced.template.md +67 -0
  162. package/content/templates/feature-tasks.template.md +60 -0
  163. package/content/templates/focus.template.md +30 -0
  164. package/content/templates/gotcha.template.md +31 -0
  165. package/content/templates/guardrail-runner.template.md +81 -0
  166. package/content/templates/guardrail.template.md +63 -0
  167. package/content/templates/learning.template.md +40 -0
  168. package/content/templates/procedure.template.md +47 -0
  169. package/content/templates/protocols/p1.md +29 -0
  170. package/content/templates/protocols/p2.md +28 -0
  171. package/content/templates/protocols/p3.md +47 -0
  172. package/content/templates/research.template.md +50 -0
  173. package/content/templates/session-handoff.template.md +58 -0
  174. package/content/templates/us.template.md +39 -0
  175. package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
  176. package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
  177. package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
  178. package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
  179. package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
  180. package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
  181. package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
  182. package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
  183. package/content/tools/gate/core-rules/README.md +55 -0
  184. package/content/tools/gate/guardrails.sh +740 -0
  185. package/content/tools/gate/lib/ratchet.sh +119 -0
  186. package/content/tools/gate/lib/scope.sh +85 -0
  187. package/content/tools/gate/ratchet.seed.tsv +1 -0
  188. package/content/tools/gate/rules/README.md +50 -0
  189. package/package.json +19 -5
  190. package/plugin/agy/hooks.json +29 -0
  191. package/plugin/agy/mcp_config.json +7 -0
  192. package/plugin/agy/plugin.json +6 -0
  193. package/plugin/agy/skills/afm-adr/SKILL.md +44 -0
  194. package/plugin/agy/skills/afm-analyze/SKILL.md +36 -0
  195. package/plugin/agy/skills/afm-clarify/SKILL.md +36 -0
  196. package/plugin/agy/skills/afm-deliver/SKILL.md +44 -0
  197. package/plugin/agy/skills/afm-diagnose/SKILL.md +50 -0
  198. package/plugin/agy/skills/afm-evolve/SKILL.md +69 -0
  199. package/plugin/agy/skills/afm-export-template/SKILL.md +42 -0
  200. package/plugin/agy/skills/afm-generalize/SKILL.md +49 -0
  201. package/plugin/agy/skills/afm-gotcha/SKILL.md +42 -0
  202. package/plugin/agy/skills/afm-init/SKILL.md +84 -0
  203. package/plugin/agy/skills/afm-plan/SKILL.md +36 -0
  204. package/plugin/agy/skills/afm-policy/SKILL.md +31 -0
  205. package/plugin/agy/skills/afm-promote/SKILL.md +41 -0
  206. package/plugin/agy/skills/afm-protocol/SKILL.md +42 -0
  207. package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
  208. package/plugin/agy/skills/afm-refactor/SKILL.md +86 -0
  209. package/plugin/agy/skills/afm-reflect/SKILL.md +55 -0
  210. package/plugin/agy/skills/afm-remediate/SKILL.md +41 -0
  211. package/plugin/agy/skills/afm-restructure/SKILL.md +54 -0
  212. package/plugin/agy/skills/afm-rule/SKILL.md +46 -0
  213. package/plugin/agy/skills/afm-specify/SKILL.md +36 -0
  214. package/plugin/agy/skills/afm-tasks/SKILL.md +34 -0
  215. package/plugin/agy/skills/afm-update/SKILL.md +58 -0
  216. package/plugin/agy/skills/afm-verify/SKILL.md +33 -0
  217. package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
  218. package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
  219. package/plugin/claude-code/.mcp.json +7 -0
  220. package/plugin/claude-code/agents/afm-executor.md +18 -0
  221. package/plugin/claude-code/agents/afm-explorer.md +20 -0
  222. package/plugin/claude-code/agents/afm-reviewer.md +21 -0
  223. package/plugin/claude-code/hooks/hooks.json +85 -0
  224. package/plugin/claude-code/skills/adr/SKILL.md +44 -0
  225. package/plugin/claude-code/skills/analyze/SKILL.md +36 -0
  226. package/plugin/claude-code/skills/clarify/SKILL.md +36 -0
  227. package/plugin/claude-code/skills/deliver/SKILL.md +44 -0
  228. package/plugin/claude-code/skills/diagnose/SKILL.md +50 -0
  229. package/plugin/claude-code/skills/evolve/SKILL.md +69 -0
  230. package/plugin/claude-code/skills/export-template/SKILL.md +42 -0
  231. package/plugin/claude-code/skills/generalize/SKILL.md +49 -0
  232. package/plugin/claude-code/skills/gotcha/SKILL.md +42 -0
  233. package/plugin/claude-code/skills/init/SKILL.md +84 -0
  234. package/plugin/claude-code/skills/plan/SKILL.md +36 -0
  235. package/plugin/claude-code/skills/policy/SKILL.md +31 -0
  236. package/plugin/claude-code/skills/promote/SKILL.md +41 -0
  237. package/plugin/claude-code/skills/protocol/SKILL.md +42 -0
  238. package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
  239. package/plugin/claude-code/skills/refactor/SKILL.md +86 -0
  240. package/plugin/claude-code/skills/reflect/SKILL.md +55 -0
  241. package/plugin/claude-code/skills/remediate/SKILL.md +41 -0
  242. package/plugin/claude-code/skills/restructure/SKILL.md +54 -0
  243. package/plugin/claude-code/skills/rule/SKILL.md +46 -0
  244. package/plugin/claude-code/skills/specify/SKILL.md +36 -0
  245. package/plugin/claude-code/skills/tasks/SKILL.md +34 -0
  246. package/plugin/claude-code/skills/update/SKILL.md +58 -0
  247. package/plugin/claude-code/skills/verify/SKILL.md +33 -0
  248. package/plugin/opencode/afm.js +173 -0
  249. package/plugin/opencode/agents/afm-executor.md +31 -0
  250. package/plugin/opencode/agents/afm-explorer.md +47 -0
  251. package/plugin/opencode/agents/afm-reviewer.md +48 -0
  252. package/plugin/opencode/skills/afm-adr/SKILL.md +44 -0
  253. package/plugin/opencode/skills/afm-analyze/SKILL.md +36 -0
  254. package/plugin/opencode/skills/afm-clarify/SKILL.md +36 -0
  255. package/plugin/opencode/skills/afm-deliver/SKILL.md +44 -0
  256. package/plugin/opencode/skills/afm-diagnose/SKILL.md +50 -0
  257. package/plugin/opencode/skills/afm-evolve/SKILL.md +69 -0
  258. package/plugin/opencode/skills/afm-export-template/SKILL.md +42 -0
  259. package/plugin/opencode/skills/afm-generalize/SKILL.md +49 -0
  260. package/plugin/opencode/skills/afm-gotcha/SKILL.md +42 -0
  261. package/plugin/opencode/skills/afm-init/SKILL.md +84 -0
  262. package/plugin/opencode/skills/afm-plan/SKILL.md +36 -0
  263. package/plugin/opencode/skills/afm-policy/SKILL.md +31 -0
  264. package/plugin/opencode/skills/afm-promote/SKILL.md +41 -0
  265. package/plugin/opencode/skills/afm-protocol/SKILL.md +42 -0
  266. package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
  267. package/plugin/opencode/skills/afm-refactor/SKILL.md +86 -0
  268. package/plugin/opencode/skills/afm-reflect/SKILL.md +55 -0
  269. package/plugin/opencode/skills/afm-remediate/SKILL.md +41 -0
  270. package/plugin/opencode/skills/afm-restructure/SKILL.md +54 -0
  271. package/plugin/opencode/skills/afm-rule/SKILL.md +46 -0
  272. package/plugin/opencode/skills/afm-specify/SKILL.md +36 -0
  273. package/plugin/opencode/skills/afm-tasks/SKILL.md +34 -0
  274. package/plugin/opencode/skills/afm-update/SKILL.md +58 -0
  275. package/plugin/opencode/skills/afm-verify/SKILL.md +33 -0
  276. package/README.md +0 -3
@@ -0,0 +1,4256 @@
1
+ # Changelog — framework-template
2
+
3
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning [SemVer](https://semver.org/lang/pt-BR/).
4
+
5
+ > **Older releases live in [`CHANGELOG-archive.md`](./CHANGELOG-archive.md).** Everything
6
+ > from `3.0.0-rc.1` through `3.4.0-rc.7` moved there on 2026-08-23, verbatim and
7
+ > uncompressed. `ops/update.md` must read the archive too when the version range reaches
8
+ > back before `3.5.0`.
9
+
10
+ ## [4.0.0-alpha.5] - 2026-10-08
11
+
12
+ A shipped copy of the framework whose provenance is proven untouched is brought up to date by the update.
13
+
14
+ ### Fixed
15
+
16
+ - **A feature folder takes one status.** A file that declares none takes the value its declaring siblings agree
17
+ on; a folder whose files disagree is held and named in the output, never silently. A prose mention of a
18
+ `status` key is not a declaration.
19
+ - **An approval of a plan is not a lifecycle state.** `Plan status: approved` stays as prose and no longer holds
20
+ a done folder.
21
+ - **A `research.md` inside a feature folder** written as `type: research` becomes `feature-research`, and its
22
+ 3.x status (`draft`, `reviewed`, `applied`) maps to `open` or `closed`, the word kept as prose.
23
+ - **A trigger that agrees is not a contradiction:** a truncated prefix of the body `Trigger`, a value equal to it
24
+ once whitespace and punctuation are normalised, a glob that covers a path the body names, or a glob beside a
25
+ body sentence that names no path (the glob becomes `trigger_path`).
26
+ - **`afm update --refresh <path>` and `--all-shipped`** are two alternatives with a clear usage error.
27
+
28
+ ### Consumer migration
29
+
30
+ - **shipped-copy-sync.** A materialized rubric, pattern or ADR template whose provenance is proven untouched (its manifest hash, or a text an earlier release shipped) is replaced with the current shipped text; a copy of unknown provenance or an edited one is left for afm update --refresh. (ops: custom)
31
+
32
+ ## [4.0.0-alpha.4] - 2026-10-08
33
+
34
+ A feature folder with no status gets one derived from its tasks.md.
35
+
36
+ ### Consumer migration
37
+
38
+ - **feature-status-derived.** A feature folder (docs/features/NNN-slug/) where no file declares a status takes one derived from its tasks.md (all ticked: done, some: in_progress, none: planned, no tasks.md: draft) in the frontmatter of its spec, plan and tasks. (ops: custom)
39
+
40
+ ## [4.0.0-alpha.3] - 2026-10-08
41
+
42
+ Templates mark their explanatory passages as guidance, so a generated document carries only what its
43
+ author fills in.
44
+
45
+ ### Changed
46
+
47
+ - **Fifteen templates** wrap their explainers (blockquotes, italic footers, example comments) in
48
+ `<!-- guidance -->` ... `<!-- /guidance -->`; `afm new`, `afm adr` and `afm gotcha` strip them.
49
+ - **The gotcha template** marks `bites` and `last_bite` optional.
50
+ - **The core spec** knows `materializes_to` and `status: deprecated`.
51
+
52
+ ### Consumer migration
53
+
54
+ - **adr-template-guidance-markers.** The ADR template (docs/adr/0000-template.md) marks its explanatory passages as guidance, which afm adr strips from the ADRs it writes. (ops: sync)
55
+
56
+ ## [4.0.0-alpha.2] - 2026-10-08
57
+
58
+ The review of wave B: the lift, the lint and the codemod engine stop guessing, and the doctrine text says
59
+ what a number or a rule may claim.
60
+
61
+ ### Fixed
62
+
63
+ - **The OKF lift** hands a contradiction (a body value that disagrees with the frontmatter) to the agent
64
+ instead of picking one, and a learning is injected from its frontmatter. A lifted `Trigger:` line that
65
+ names a path in backticks becomes `fires_on: file-shape` with that `trigger_path`, so the pre-edit hook
66
+ delivers the lifted gotcha or learning; with no path it stays an `event`.
67
+ - **`afm lint`** stays inside the roots the index reads and judges only the scope it was given, and
68
+ `--fix --json` carries the items the lift could not resolve.
69
+ - **`afm rule --does-not-cover`** refuses a blank value instead of writing a rule that fails its schema.
70
+ - **The codemod engine**: `sync` has three outcomes (overwrite a pristine file, hold a file the owner
71
+ accepted against the same shipped text, hand any other edited file to the agent), the shipped hash is
72
+ recorded at `--accept` time, and `--dry-run --accept <path>` writes nothing.
73
+ - **The failure-classification rubric** has its decision tree in English.
74
+
75
+ ### Changed
76
+
77
+ - **Rules and guardrails** state what they do not cover (`does_not_cover`), and a rule statement names
78
+ its scope.
79
+ - **Deliver** numbers carry provenance; **remediate** needs a red proof first; **analyze** has an
80
+ invention class; the architecture guide says how an import-rule exit is read; the feature-tasks
81
+ template notes the version bump.
82
+ - **The ADR template** no longer carries a body metadata block.
83
+
84
+ ### Consumer migration
85
+
86
+ - **record-untouched-files.** A materialized file with no manifest entry whose text is what init writes today, or exactly what an earlier release wrote for the failure-classification rubric or the ADR template, is recorded as untouched so the syncs below can update it. (ops: custom)
87
+ - **failure-classification-english.** The failure-classification rubric's decision tree is in English, and its guardrail origin keys name the English frontmatter. (ops: sync)
88
+ - **adr-template-no-body-metadata.** The ADR template (docs/adr/0000-template.md) keeps status, date and decider in the frontmatter only, with no body metadata block. (ops: sync)
89
+
90
+ ## [4.0.0-alpha.1] - 2026-10-07
91
+
92
+ Metadata moves into the frontmatter, one strict schema per OKF type (ADR-0014), and `afm update` runs
93
+ typed codemods instead of reading prose (ADR-0013).
94
+
95
+ ### Added
96
+
97
+ - **One schema per OKF type** (`packages/core/src/dsl/okf/schema/`): an unknown type or a retired `kind:` is an
98
+ error, a missing frontmatter is a warning outside a declared family, and a write that breaks a
99
+ blocking family (adr, rule, gotcha, feature-spec, feature-plan, feature-tasks, guardrail) is refused.
100
+ - **The codemod registry**: each release declares ordered, typed ops (replace, move, create, sync,
101
+ delete, frontmatter, section, regenerate, custom, agent). The run proves each op's need before a
102
+ write, hands judgment to the agent as a task with a 0/1 check, and moves the pin only when no task is
103
+ pending.
104
+ - **`docs/.afm-manifest.json`**: the hash of every file AFM materialized, so an update overwrites only a
105
+ file the owner never edited. `afm update --accept <path>` records an edited file the agent merged.
106
+
107
+ ### Changed
108
+
109
+ - **Hard rule PLUGIN-39 and the architecture guide** name the per-type schema, `afm lint` as its verification, and `okf/` and `codemods/` in the folder map. Op docs write documents through `afm adr`, `afm gotcha`, `afm rule` and `afm new <type>`; the shipped guardrail catalog is type `guardrail-suggestion`.
110
+ - **Metadata lives only in the frontmatter.** A body starts at its H1: no `> **Status:**`,
111
+ `**Trigger:**` or `- **Origin:**` lines. A feature folder has one `status` (draft, clarified,
112
+ planned, in_progress, done, cancelled); a gotcha keeps `bites`, `last_bite` and `refs` in the
113
+ frontmatter and its Behavior and Solution as `## Behavior` and `## Solution` sections; a guardrail
114
+ cost is low, medium or high.
115
+
116
+ ### Consumer migration
117
+
118
+ - **okf-lift.** Every docs file in an OKF family, or with a frontmatter, moves its metadata into a frontmatter its type's schema accepts; what cannot be converted exactly goes to the agent. (ops: custom)
119
+ - **manifest-bootstrap.** An instance with no manifest records every rubric, pattern and ADR template that still equals what init writes today; any other file stays unrecorded and is treated as edited. (ops: custom)
120
+ - **plugin-39-per-type-schema.** Hard rule PLUGIN-39 in docs/afm.md says each type has its own schema and that afm lint is the verification. (ops: replace x2)
121
+ - **rule-does-not-cover.** Hard rules in docs/afm.md say a rule or guardrail may state, in an optional does_not_cover key, what its gate does not check. (ops: replace)
122
+ - **ach-boundary-exit.** The import rules in docs/ach.md say a boundary holds from the first need: a primitive needed on the forbidden side moves to the lowest shared layer. (ops: section)
123
+
124
+ ## [3.43.0] — 2026-09-17
125
+
126
+ `web_read` leaves `afm`. It moves to the new `research` plugin, which stores each page once and
127
+ scores sources across campaigns (ADR 0027).
128
+
129
+ ### Removed
130
+
131
+ - **The `research` MCP server** (`mcp/`, `.mcp.json`). `afm` no longer starts an MCP server, and
132
+ the lazy `npm install` and build on first use are gone with it.
133
+
134
+ ### Changed
135
+
136
+ - **`ops/research.md`** names the `research` plugin as the source of `web_read`. Without that
137
+ plugin, the op runs in WebFetch mode, as it already did when the server could not start. The op
138
+ also points a large, many-subject corpus to `/research:campaign`.
139
+
140
+ ### Consumer migration
141
+
142
+ - **Consumer-noop.** `CONSUMER_VERSION` stays at `3.41.0`. Nothing materialized into
143
+ `<consumer>/docs/` changes. To keep `web_read`, install the `research` plugin.
144
+
145
+ ## [3.42.0] — 2026-09-11
146
+
147
+ `afm-capture-failure.sh` mislabeled three shapes of oracle output — measured in this repository
148
+ and in kandex — filling the `remediate` substrate with signatures no procedure could fix.
149
+
150
+ ### Fixed
151
+
152
+ - **A signal exit was not a timeout.** The guard was a literal `Exit code 124|143` list, and any
153
+ process killed by a signal exits 128+N — so `Exit code 144` (a `pkill -f` that matched its own
154
+ shell, recorded 10 times in this repository's own ledger:
155
+ `docs/.afm-log-failures/2026-09-03-test-exit-code.md`, and 3 times in kandex) and `Exit code 137`
156
+ (SIGKILL) both slipped past the guard and were counted as real oracle failures. The guard now
157
+ checks the FIRST line for `Exit code N` with `N` equal to 124 or in `129..159` (128 + every
158
+ signal number 1-31). The crash signals stay oracle failures: SIGILL, SIGTRAP, SIGABRT, SIGBUS,
159
+ SIGFPE and SIGSEGV (132-136, 138, 139). A test runner that aborts or segfaults reports on the
160
+ code under test, the same as an assertion does. The guard also skips a line that STARTS with
161
+ `Command timed out after`, which Claude Code can insert on its own. A test whose name only
162
+ mentions a timeout still counts.
163
+ - **The signature keyed on a stable banner, not on the error.** vitest prints `Testing types with
164
+ tsc and vue-tsc is an experimental feature.` on every run, right after the exit code — and the
165
+ old signature normalized the WHOLE error text, so two different failing tests collapsed into one
166
+ `sig=test-testing-types-…`, and a fixed test still looked like the same recurring failure. The
167
+ signature source is now the first error-looking line (`error|fail|✗|×|assert|TS[0-9]+`,
168
+ case-insensitive) found AFTER the `Exit code N` line, falling back to the old whole-text
169
+ normalization when there is no such line — so a payload with no exit code (an older capture, a
170
+ non-Bash tool) keeps working exactly as before. A runner summary such as `Tests: 1 failed, 24 passed`
171
+ prints on every failing run whatever failed, so a line that says `passed` is never the key.
172
+ - **A heredoc's body is data, and its words read as oracle verbs.** An `ste-lint` probe whose
173
+ scratch heredoc happened to say "run the test suite and check the build" was captured as
174
+ `sig=test-…` (`docs/.afm-log-failures/2026-09-05-test-exit-code-block-s1-file-procedure-senten.md`),
175
+ even though `ste-lint` itself was never the failure. Heredoc BODIES (the lines between the
176
+ opening `<<DELIM` and the closing `DELIM`) are now stripped from the command before verb
177
+ matching, on the raw command and before the existing quote-stripping. The strip is bodies only,
178
+ never the whole command: a real oracle chained after a heredoc (`cat > f.json <<EOF\n{}\nEOF\nnpm
179
+ test`) still counts, which a guard that skipped every heredoc-bearing command outright would have
180
+ lost. This also exposed a second latent bug in the same guard — the `cat …` reader-shortcut
181
+ matched a heredoc's opening `cat > file <<EOF` line too (a WRITE, not a read) and, being a shell
182
+ `case` glob, matched across the newlines into whatever came after it, so the real oracle
183
+ chained after the heredoc never got a chance to be classified. The shortcut now steps aside
184
+ whenever the command contains a heredoc redirect.
185
+
186
+ New asserts pin this in `hooks/test/run.sh`, next to the existing failure-substrate
187
+ section, using the real payloads above.
188
+
189
+ ### Consumer migration
190
+
191
+ - **Consumer-noop.** `CONSUMER_VERSION` stays at `3.41.0` — nothing materialized into
192
+ `<consumer>/docs/` changes. Every change here is in `hooks/`, which ships with the plugin copy
193
+ and reaches an instance through `claude plugin update`, never through materialization.
194
+
195
+ ## [3.41.0] — 2026-09-04
196
+
197
+ The structural writing gate this repository ships as a plugin now runs on the methodology written
198
+ next to it. See [ADR 0025](../../../docs/adr/0025-o-portao-de-escrita-roda-no-proprio-plugin.md).
199
+
200
+ ### Added
201
+
202
+ - **Hard rule 35 — the methodology text passes `ste-lint --scope strict`**, with a runner at
203
+ `test/rules/35-ste-strict.sh` wired into `test/guardrails.sh`.
204
+
205
+ **Why, measured.** `CHANGELOG.md:505` records a past release: *"29 `block` findings in
206
+ `ste-lint --scope strict` across 7 op files… Now 0 across all 48 skills and ops."* Run by hand,
207
+ declared zero, wired to nothing — `grep -rn ste-lint test/ .githooks/` returned nothing.
208
+ Measured again on 2026-09-04: `ops/` was back to **25** and `core/` had **89**, which had never
209
+ been in scope. `skills/` was still 0. A number that only a person remembers to measure is not a
210
+ measurement, it is a memory.
211
+
212
+ **It is a LOCAL rule and never reaches a consumer.** Under ADR-0021 a plain `<n>` stays here.
213
+ ASD-STE100 is this project's convention for its own text; imposing a writing style on the ~37
214
+ projects that adopted AFM is the imposition 3.40.0 removed when it split `modules/` from
215
+ `code-style/`. Only `block` fails — `advise` never does, for the reason the linter's own header
216
+ gives.
217
+
218
+ ### Changed
219
+
220
+ - **The 114 blocking findings are fixed, so the gate enters at 3 and not at 114.** A gate that
221
+ starts red is turned off the next day. The 3 that remain are declared in
222
+ `test/baseline/ratchet.tsv`: three `*Verification:*` lines whose facts cannot be cut and whose
223
+ line cannot be split — `afm-health.sh`'s A5 axis selects LINES matching `verifica` and reads the
224
+ backticked paths off them, so a second physical line silently drops every path on it.
225
+
226
+ - **Prose names the native tool it means.** *"stop and ask"* becomes *"stop and ask through
227
+ `AskUserQuestion`"* where the sentence is about asking.
228
+
229
+ **And it stays abstract where the framework abstracts on purpose.** Three of the first six
230
+ namings were rejected by the adversarial pass, correctly: `core/protocol.md`'s P3 gate was named
231
+ `ExitPlanMode`, but `templates/protocols/p3.md:45` says the opposite in the same breath — *"the
232
+ native mode changes the approval surface, never the criterion"*. Hard rule 11's gate is
233
+ dual-surface by design (`core/afm.md`: *"`ExitPlanMode`, or `AskUserQuestion`"*), so it keeps its
234
+ abstraction. Naming one surface where two are documented is a narrowing, not precision.
235
+
236
+ - **`PLUGIN-2`'s verification gains a native tier, and keeps its fallback.** Where the project
237
+ already configures the no-explicit-any rule in ESLint or Biome, that command is the trigger.
238
+ Where it does not, the `grep` stays, unchanged. A repository with no linter behaves exactly as
239
+ before, byte for byte. Same reasoning as ADR-0015 for the commit gate, applied to a rule trigger.
240
+
241
+ The rule ids are spelled out rather than quoted as paths: A5 resolves every backticked path on a
242
+ `*Verification:*` line against the disk, and a package-scoped rule id looks exactly like one.
243
+ Writing it the obvious way created a false vital sign, caught before commit.
244
+
245
+ - **Five `> **Ancestor:**` blockquotes become pseudo-algorithms.** In `generalize`, `restructure`,
246
+ `reflect`, `evolve` and `diagnose`, the citation's prose was the file's ONLY definition of a term
247
+ the body then reused — `ShowsImprovement` 6 times, `Staleness` 7, `Reflect` 6, `Commit` 5,
248
+ `ANALYZER` 4. The citation stays on the first line and the prose becomes a named algorithm that
249
+ defines each term with `:=`. `ops/remediate.md` keeps its blockquote: there the citation really
250
+ is only attribution. **No citation was deleted** — 15 before, 15 after.
251
+
252
+ ### The invariants this release was measured against
253
+
254
+ Every one held, verified by diff and not by count: the 34 rule headlines
255
+ `afm-session-start.sh:46` extracts are **byte-identical**; the 22 paths A5 reads off
256
+ `*Verification:*` lines are **byte-identical**; 47 `{{placeholders}}`, 151 `✅`/`❌`, 15 `arXiv`,
257
+ 2 `[TBD]`.
258
+
259
+ Ten deliveries went through an adversarial verifier and **five were rejected**, all for the same
260
+ class: a qualifier dropped to satisfy a word count. A conditional turned unconditional in the saga
261
+ rule, *"or you said 'go'"* deleted from a trigger whose own headline still named it, *"only"* and
262
+ *"and correct"* and *"against the approach"* removed. Each was restored by splitting the sentence
263
+ instead of cutting the qualifier, which is what the gate asks for in the first place.
264
+
265
+ ### Consumer migration
266
+
267
+ - **Consumer-facing.** `CONSUMER_VERSION` moves to `3.41.0`. `core/*.md` and `PRINCIPLES.md`
268
+ materialize into an instance.
269
+ - **The delta worth taking is `PLUGIN-2`'s tiered verification** — if your project runs ESLint or
270
+ Biome with the no-explicit-any rule, that command becomes the trigger and the `grep` stops
271
+ producing false positives on identifiers that merely contain `any`.
272
+ - **The STE rewrite is the plugin's own hygiene, and `update` will not force it on you.**
273
+ `ops/update.md:216` — *"update never deletes"* — so `update` materializes what your instance
274
+ lacks and never rewrites prose you edited by hand. Hard rule 35 is local to the plugin's
275
+ repository and is **not** materialized: your `docs/` is not subject to ASD-STE100 unless you
276
+ choose it.
277
+
278
+ ## [3.40.3] — 2026-09-04
279
+
280
+ The resident surface stops carrying what is not resident-worthy. A skill's frontmatter is loaded
281
+ into every turn; its body and its `ops/*.md` are read only when the skill runs. Procedure had
282
+ accumulated in the wrong one of the two.
283
+
284
+ This came out of an external audit of the plugin, whose telemetry was real and reproduced exactly,
285
+ and whose conclusions did not survive measurement — see the release notes below for what was
286
+ rejected and why.
287
+
288
+ ### Changed
289
+
290
+ - **The 25 skill descriptions carry routing, not procedure.** Each one now states what the skill
291
+ writes or decides, then lists every trigger phrase under `Activates`. What left is execution
292
+ contract that already lives in `ops/*.md` and in the skill body: `specify`'s six overhead
293
+ thresholds, `diagnose`'s "Role: ANALYZER / does not open a gate / an empty diagnosis is valid",
294
+ and similar. Frontmatter goes from **11,010 to 9,804 chars** (~300 tokens per turn), and
295
+ `ste-lint --scope flavored` goes from **42 findings to 9**.
296
+
297
+ **No trigger phrase was removed**, and that is the assertion that matters: routability is what
298
+ `docs/adr/0012-*` certified with 100 eval trials, so touching it would mean re-running
299
+ `test/evals/`. A mechanical diff against the previous frontmatter confirms every quoted phrase
300
+ survives; three were *split* rather than dropped (`"clean up/reorganize the docs"` became two
301
+ explicit phrases), which is strictly more matchable.
302
+
303
+ - **`plugin.json`'s description goes from 2,013 chars to 172.** It was a catalogue of the 25
304
+ skills — every one of which already ships its own resident description. That is hard rule 34
305
+ (a fact stated twice drifts, and the copy goes stale first), and it routes nothing: skills route.
306
+ ~460 tokens per turn.
307
+
308
+ ### Not changed, and why
309
+
310
+ An external report proposed five cuts. Twelve agents tested them, one arguing for and one against
311
+ each, and the measurements are in `docs/research/` if they are wanted later. The short form:
312
+
313
+ - **Merging 25 skills into 5** — `docs/adr/0003-*` already built exactly that (router skills over
314
+ N operations) and reverted it: the router "hid the operations behind a router" and made
315
+ natural-language activation hard. ADR-0012 then certified the current shape with 100 trials.
316
+ - **Removing the blocking `Stop` arm** — reflect ran 23 times, evolve twice, and the three ops
317
+ with no trigger ran zero. That is one measurement of 115 invocations, not three anecdotes.
318
+ - **Silencing `PreToolUse`** — it costs **681 tokens per 1000 responses** in this repo against
319
+ 5,098 in the audited sample, which is pinned at `3.5.0-rc.1`. The cost was fixed 35 releases ago.
320
+ - **Cutting `SessionStart` to three lines** — every AFM hook together injected 61,635 tokens
321
+ against 560,987,629 read in the audited sample: **0.011%**. `afm-token-audit.sh` reports a true
322
+ per-1000-responses rate that invites a false comparison against per-turn context.
323
+
324
+ ### Consumer migration
325
+
326
+ - **Consumer-noop.** `CONSUMER_VERSION` stays at `3.40.0`. Nothing materialized into
327
+ `<consumer>/docs/` changes; `skills/` and `.claude-plugin/` ship with the plugin copy and reach
328
+ an instance through `claude plugin update`.
329
+
330
+ ## [3.40.2] — 2026-09-04
331
+
332
+ Three defects in `hooks/`, found by an adversarial pass over the eleven gaps left open after
333
+ 3.40.1. They share a shape: each mechanism reported a state it had not measured. One counted
334
+ commits that never happened, one went quiet because the artifact it polled is written once, and
335
+ both `Stop` arms printed an escape hatch neither of them read.
336
+
337
+ ### Fixed
338
+
339
+ - **The commit event counted mentions.** The classifier was `case "$CMD" in *"git commit"*)` — a
340
+ glob over the command STRING, so any command that merely CONTAINED the text produced a
341
+ `task | commit` event: an `echo`, a heredoc, and (measured on 2026-09-04) a `grep` reading this
342
+ very line. That counter arms the reflect `Stop` arm, so every phantom pulled the cadence
343
+ forward. The substring stays as the pre-filter and **git is now the discriminator**: HEAD must
344
+ have moved within the last two minutes, and its SHA must not already be in the ledger — so a
345
+ mention right after a real commit resolves to the commit already recorded. The event now
346
+ carries the SHA it claims. Not `tool_response`: its field names are documented nowhere here and
347
+ `git commit -q` prints no success line, so a stdout pattern would trade false positives for
348
+ silent false negatives. Regression tests run in **both** directions — three real mention shapes
349
+ record nothing, and a real commit through `xargs`/`sh -c` with `-q` is still recorded.
350
+ - **The reflect arm saturated.** It gated on the session file's `reflection:` field being empty.
351
+ That field is **write-once**: filled the first time and still filled ten days later, so a
352
+ resumed session was answered once and never asked again. Measured on this repository: **164
353
+ tasks against a threshold of 20** — eight crossings — and one reflection. The gate is removed
354
+ (and `afm_reflection_pending` with it — nothing else called it); what unblocks is the counter,
355
+ which falls when a real cycle lands on disk. That is still 0/1 over an artifact, and it is one
356
+ that moves. See [ADR 0024], which supersedes that one clause of ADR 0013 — and which states the
357
+ limit this inherits: `afm_count_since` compares dates at **day granularity**, so work done on
358
+ the same calendar day as a recorded cycle stays invisible to the counter until the next day.
359
+ That predates the change and bounds it; the motivating case (ten days, no cycle) counts fine.
360
+ - **G2 counted sessions, not crossings.** One block per `session_id` is a lifetime allowance for a
361
+ session that never ends. The flag now carries the crossing index and the cycle epoch, so another
362
+ full threshold of work re-arms the block, and crossing 1 *after* a recorded cycle is a new event
363
+ rather than the one already answered. Applies to both arms.
364
+ - **The reflect arm counted in the evolve window.** `afm_count_since deliverable evolve`, inside
365
+ `reflect_arm` — so the arm could block on a number that `/afm:reflect`, the remedy its own
366
+ message prescribes, cannot lower: only an evolve cycle resets that window. ADR 0020 already
367
+ required the opposite (*"the window belongs to the mechanism"*). The old `reflection:` gate hid
368
+ it by unblocking unconditionally; removing the gate exposed it, and an adversarial pass
369
+ reproduced the arm re-blocking on the very next `Stop` right after the operator complied.
370
+ - **The `debt: true` escape hatch was prose.** Both arms print "add `debt: true` to the session
371
+ file's frontmatter" as their second path, and neither read the marker:
372
+ `docs/sessions/2026-08-25-1734-cb06b3.md` had carried it since 2026-08-25 and the evolve arm
373
+ blocked regardless. Now honoured, and **session-scoped** on purpose, unlike the crossing cap:
374
+ the cap bounds how often a mechanism speaks unprompted, while the marker is the operator having
375
+ answered, and re-asking at every threshold would be nagging against a declaration. The debt
376
+ stays visible — `afm-health.sh` reports it in every SessionStart digest.
377
+
378
+ A first draft of this marked the crossing as handled on the debt path, which made the
379
+ suppression **irreversible**: withdrawing `debt: true` left the flag on disk, the cap fired
380
+ before the marker was re-read, and the arm went silent for good on that crossing. The suite
381
+ passed, because the test cleared the flag in between — scaffolding the real hook never runs.
382
+ That is this repository's signature defect reproduced inside the fix for it. The marker is now
383
+ read before the cap and writes no flag, and the regression test asserts the withdrawal
384
+ **without** clearing anything.
385
+
386
+ ### Added
387
+
388
+ - **`hooks/lib/afm-root.sh`** — one answer to *which root does this event belong to*.
389
+ `afm-pre-edit.sh` and `afm-capture.sh` each carried their own copy of the same three
390
+ `case "$PWD"/*` arms, and `docs/research/011-afm-em-monorepo.md` found the copies already
391
+ disagreeing with each other and with `tools/afm-detect.sh`; all four candidate monorepo designs
392
+ failed on that. A missing library falls back to an **inline** definition of the same functions,
393
+ never to nothing — an unset `afm_root` under `set -u` would collapse the guard's `"$_r"/*` arm
394
+ to `/*`, which matches every absolute path, and the hooks would act on other repositories while
395
+ every test still passed.
396
+
397
+ **This closes only half the gap, and says so.** Six of the eight hooks receive no file path at
398
+ all in their payload. That is a harness limit, not an AFM one, and no resolver can close it; the
399
+ federated monorepo designs stay blocked on it.
400
+
401
+ ### Consumer migration
402
+
403
+ - **Consumer-noop.** `CONSUMER_VERSION` stays at `3.40.0` — nothing materialized into
404
+ `<consumer>/docs/` changes, and no instance needs `/afm:update`. Every change here is in
405
+ `hooks/`, which ships with the plugin copy and reaches an instance through
406
+ `claude plugin update`, never through materialization.
407
+ - **One behavioural note for an operator running a long session.** A resumed session that used to
408
+ be asked for a reflection once will now be asked once per accumulated threshold. If that is not
409
+ wanted, the two declared ways to stop it are unchanged and both still work: `debt: true` in the
410
+ session file (now actually read), or `cadence: reflect=off` / `evolve=off` in `docs/_focus.md`.
411
+
412
+ [ADR 0024]: ../../../docs/adr/0024-o-desbloqueio-da-reflect-e-o-contador-nao-o-campo.md
413
+
414
+ ## [3.40.1] — 2026-09-04
415
+
416
+ The two fixes the monorepo research isolated as true regardless of which architecture is eventually
417
+ chosen (`docs/research/011-*`). Neither needs an answer to the federated-versus-single question, so
418
+ neither waits for one.
419
+
420
+ ### Fixed
421
+
422
+ - **A stack detector selected the package that PRODUCES the scope.** The five dependency-based
423
+ `detect:` commands grepped the whole manifest, so a package whose own `"name"` matched the
424
+ pattern fired the module built for its consumers. Measured on one design system: **seven of its
425
+ packages** selected `vireya-ui` against themselves. Each detector is now a `jq` query scoped to
426
+ `dependencies`/`devDependencies`/`peerDependencies` — a `"name"` field is never inside those
427
+ three objects, so the self-match closes structurally rather than by a better pattern. Verified:
428
+ the package that only produces stopped matching, the two that genuinely depend on the scope
429
+ still match. Where `jq` is absent the command falls back to the previous `grep`, so a machine
430
+ without it keeps exactly today's behaviour and loses nothing.
431
+ - **The stack scan stopped at the front door.** Both interviews ran each `detect:` once, at the
432
+ consumer root. A workspace root declares the tooling and none of the stacks, so the scan
433
+ answered "no stacks here" for a repository full of them: measured across the nine real JS/TS
434
+ workspaces in the operator's tree, **63 root-level probes, 63 empty**, while the same commands
435
+ fire correctly inside the packages — Next.js in seven different `apps/*`, tRPC in five places.
436
+ `bootstrap` and `refactor` now enumerate the manifests first and scan each, and both are told to
437
+ record **where** each stack was found: a stack present in one package out of eight is a true
438
+ finding and a different fact from one present throughout.
439
+
440
+ ### Consumer migration
441
+
442
+ - **Consumer-noop.** `CONSUMER_VERSION` stays at `3.40.0` — nothing materialized into
443
+ `<consumer>/docs/` changes, and no instance needs `/afm:update`. What changes is which modules a
444
+ **future** `/afm:init` or `/afm:refactor` selects, and that is read from the plugin at the moment
445
+ it runs. An instance bootstrapped inside a workspace before this release selected no stack
446
+ modules at all; re-running the stack scan by hand is the only way to recover that, and the owner
447
+ decides whether it is worth it.
448
+
449
+ ## [3.40.0] — 2026-09-04
450
+
451
+ Three defects with one root: the plugin declared as norm what it should have proposed, and
452
+ proposed what it should have mechanized. Measured across 115 `/afm:*` invocations in 26 projects,
453
+ `reflect` ran 23 times, `evolve` 2, and the three operations `evolve` conducts —
454
+ `generalize`, `restructure`, `remediate` — ran **zero**. The cause was mechanical, not cultural:
455
+ `reflect` had a blocking `Stop` arm and `evolve` had a printed suggestion. Separately, ~40% of
456
+ `modules/` turned out to be one operator's house convention shipped under `(mandatory)`, and
457
+ `core/` carried a literal `src/` in eleven places next to the placeholder that was supposed to
458
+ replace it.
459
+
460
+ ### Added
461
+
462
+ - **`reflect` escalates to `evolve`.** `ops/reflect.md` § 5 gains a step between the `promote`
463
+ branch and the `reconcile` handoff: when the findings cross categories, it proposes
464
+ `/afm:evolve` carrying the φ_b evidence already collected in §§ 1-2, never as a bare
465
+ suggestion. The discriminant is not new — `rubrics/when-to-evolve-methodology.md` already
466
+ defined it and declared `called_by: ["evolve"]`; `reflect` now consults it too. The recursion
467
+ guard is **declared**, not inherited: `evolve` runs `reflect §§ 1-2` only, so the escalation in
468
+ § 5 was already unreachable from inside the cycle — but that was accidental scoping, and there
469
+ was no recursion guard anywhere in `ops/` or `core/`.
470
+ - **`patterns/`** — seven design patterns that answer a code smell. `type: pattern` with `smell`,
471
+ `when_to_use` and `when_not_to_use`; the last of those is what keeps a library from producing a
472
+ Strategy with one strategy. It only ever suggests: a pattern becomes norm when the operator
473
+ accepts it and it materializes into `code-style/`, `ach.md` or an ADR. Excluded from PLUGIN-40
474
+ for the same reason `rubrics/` is — illustrative paths produced 44 false findings there.
475
+ - **`code-style/`** — the conventions the operator decides, as opt-in seeds.
476
+ `code-style/<stack>/<concern>.md` plus `code-style/_any/` for what belongs to no stack. A
477
+ directory-unit family like `modules/`, so it reuses the index's existing one-level descent. The
478
+ interview asks item by item and does not write what the operator declines.
479
+ - **Both families join `INDEXED`** in `tools/afm-index.sh` and in the `PLUGIN-39` gate.
480
+ `--triggers` emits one `code-style` row per stack, not one per file, and a `pattern` row
481
+ carrying its smell.
482
+
483
+ ### Changed
484
+
485
+ - **`EVOLVE_EVERY` stops being decorative.** It was read in exactly one place,
486
+ `afm-health.sh`, which built a string and printed it at `SessionStart`. `afm-session-end.sh`
487
+ read the cadence triple and **discarded** the evolve value — the underscore in
488
+ `read -r REFLECT_EVERY _EVOLVE_EVERY REFLECT_TASKS` marked the discard as deliberate. It now
489
+ has its own blocking arm, with its own one-block-per-session flag, its own `off` switch, and
490
+ the same debt escape hatch the reflection arm carries. A gate that starts red is switched off
491
+ the next day.
492
+ - **The stack modules stop imposing.** House convention moves out of all seven and into
493
+ `code-style/` seeds: naming prefixes, test placement, folder layouts, an arbitrary 80-line
494
+ ceiling with no measurement behind it, an invented helper at an invented path, and — the worst
495
+ case — four fully-qualified file paths plus six identifiers from one product's billing module.
496
+ Genuine stack fact stays: the `<html lang>` dynamic-segment constraint in Next.js, Stripe's
497
+ `SubscriptionSchedule` immutability, `ctx.run.id` changing per retry attempt, the Prisma
498
+ optimistic-locking pattern, the Vireya token system. The three `gotchas.seed.md` files were not
499
+ touched — they measured ~95% genuine.
500
+ - **`core/` paths become `{{src-path}}`.** Eleven literals in `core/afm.md` and `core/ach.md`,
501
+ and the equivalent sites in `rubrics/`, `templates/` and `interview/`. The archaeology table in
502
+ `interview/refactor.md` keeps its literals on the left — that column is how brownfield detection
503
+ recognizes a project — and parameterizes only the right, which emits the consumer's text. The
504
+ interview gains a measurement step for `{{src-path}}`, `{{domain-path}}` and `{{ext}}`, which
505
+ had no filling step in any op: a placeholder nobody fills fails silently, which is worse than a
506
+ wrong literal.
507
+ - **`core/protocol.md` stops disagreeing with the code.** It declared `REFLECT_TASKS` default `4`;
508
+ `lib/afm-cadence.sh` has hardcoded `20` since 3.9.0-rc.9, with a measurement behind it. The doc
509
+ now matches the code, and the `EVOLVE_EVERY` row names its new consumer.
510
+
511
+ ### Fixed
512
+
513
+ - **The interviews stopped naming rubrics by hand.** `bootstrap.md` and `refactor.md` each
514
+ hardcoded eight rubric filenames; twelve ship. Four rubrics had never been materialized into any
515
+ instance this framework bootstrapped. Both now query the index, the way the module list was
516
+ fixed in 3.31.0.
517
+ - **`learning.template.md` and `procedure.template.md` emitted `kind:`**, a retired key the
518
+ `PLUGIN-39` gate rejects. Two templates were generating documents that failed the project's own
519
+ gate.
520
+ - **`--for` could not read a `trigger_path` holding more than one glob.** Five ADRs write the
521
+ value as a whitespace-separated list, and `cmd_for` handed the whole string to the matcher as
522
+ ONE pattern, so it matched nothing: `--for plugins/afm/hooks/hooks.json` returned empty while
523
+ ADR-0004 declared exactly that path. Those five were unreachable through the routing the
524
+ frontmatter had just given them — a silent miss.
525
+ - **`--type protocol` printed every protocol twice.** `templates` and `templates/protocols` are
526
+ both in `INDEXED`, and the one-level descent added for `modules` re-emitted what the deeper
527
+ entry already emitted. The descent now skips a subdirectory that is itself an `INDEXED` entry;
528
+ the `PLUGIN-39` gate carried the same overlap and would have reported one finding as two.
529
+ - **The unit cache could suppress a body that was never delivered.** `afm-pre-edit.sh` marked a
530
+ gotcha or rule as seen while assembling the payload, but three later exits can still discard
531
+ it — so editing a Portuguese code file marked the rule body, delivered only the language line,
532
+ and answered the next edit with `(already delivered this session)` for something the agent had
533
+ never received. Marking is now deferred until the payload has actually left. This cache exists
534
+ to stop repetition, never to cause silence.
535
+ - **`--triggers` printed every feature's full path list**, which made `feature` the largest family
536
+ in the index at 4,218 bytes and pushed the whole output to 2.55× its previous size — against a
537
+ ratio that is the index's entire reason to exist. The row now carries the path count and points
538
+ at `--for`, which answers the question by matching instead of making the reader scan. A feature
539
+ with no paths still gets no row at all.
540
+ - **The index cost claim was stale in three places** (`CLAUDE.md`, `docs/prd.md`, the SessionStart
541
+ injection). It advertised ~2,200 tokens against ~35,000 of bodies; measured today it is ~3,700
542
+ against ~214,000. The ratio improved and the number was wrong, which is its own defect: the
543
+ claim is what convinces a reader to query instead of read.
544
+ - **`afm-detect.sh` shipped an unused second rubric directory.** `RUBRIC_DIR2` had no caller, no
545
+ test, and no guard against being set to the same path as the first, which would have doubled
546
+ every match against the cap. Removed — YAGNI.
547
+
548
+ ### Consumer migration
549
+
550
+ - **Consumer-facing.** `CONSUMER_VERSION` moves to `3.40.0`.
551
+ **This release carries two changes the bump table classifies as major** — a change in `core/`
552
+ and a move inside `framework-template/` — under a minor number, because `4.0.0` is reserved for
553
+ a paradigm change. The reason is recorded in `docs/adr/0023-*`; the bump table is not amended.
554
+ **No existing instance changes when this lands.** `ops/update.md` forbids removal and never
555
+ rewrites materialized content, and a module patch is applied once at bootstrap by an agent
556
+ reading prose — there is no re-application path. What changes is what a **new** bootstrap
557
+ materializes.
558
+ **What became obsolete, for an instance that wants to act on it:** the naming, test-placement,
559
+ file-size and import conventions that arrived from `modules/<stack>/` as `(mandatory)` are no
560
+ longer shipped as norm. An instance that wants them keeps them — they are its own now, and the
561
+ decision is recorded where the operator can see it rather than inherited from a `grep` on
562
+ `package.json`. An instance that never wanted them can delete them; `update` will not, because
563
+ deleting instance content is destructive and destructive is a bright line.
564
+ New instances receive `patterns/` and `code-style/`; the latter is offered item by item and
565
+ writes nothing the operator declines.
566
+
567
+ ## [3.31.0] — 2026-09-03
568
+
569
+ Measured, on 79 sessions across 26 consumer projects, what release 3.30.0 did to the token
570
+ bill. The frontmatter index worked: document text pulled into context fell 20% per 1000
571
+ responses, and re-reading the same document inside one session fell 74%. A hook ate the win.
572
+ `PreToolUse` injections went from 41.1 to 67.1 per 1000 responses (+63%) and from 14,565 to
573
+ 19,304 tokens per 1000 (+33%), because the structural migration split `gotchas.md` into 29
574
+ files and `afm.md` § 3 into 15, and the dedup cache keys on the FILE — so one gotcha body was
575
+ re-sent once per edited file. This release closes that leak, extends the index to the families
576
+ it could not see, and versions the instrument that measured all of it.
577
+
578
+ ### Added
579
+
580
+ - **`hooks/lib/afm-text.sh`** — `afm_truncate`, one word-safe truncation for what were ten
581
+ copies of `head -c` across three hooks. A real session received injected text ending in
582
+ "659 e": the byte cut lands mid-word, and mid-character on accented text. The helper backs
583
+ off to the last word boundary and repairs a severed UTF-8 sequence with `iconv -c`. Each
584
+ caller carries a no-op fallback, because the hook contract is "never break the session" and
585
+ a missing lib would otherwise be a hard "command not found".
586
+ - **`tools/afm-token-audit.sh`** — reads the Claude Code transcripts and reports responses,
587
+ context per turn, and hook injection cost normalized per 1000 responses. It encodes four
588
+ measured facts, each next to the code that depends on it: `message.id` repeats, so the naive
589
+ sum inflates output 2.2×; `output_tokens` in a subagent transcript is a partial streaming
590
+ snapshot and unusable; the authoritative subagent figure exists only as text in the harness
591
+ notification; and hook injections live in `attachment.stdout`, not in message content. When
592
+ the figure is absent it reports `unavailable` and never estimates.
593
+ - **Frontmatter for the seven stack modules.** `modules/<stack>/*.md` was the last family the
594
+ plugin shipped with none: 17 files that `promote` routes findings to (`target:
595
+ modules/<stack>/`) and that `bootstrap` selects from, where the only way to learn a module
596
+ existed was to list the directory. Each now carries `type: module`, `stack:`, `patches:` and a
597
+ `detect:` block scalar — one mechanical command per stack, verified against real consumer
598
+ repositories. `modules` joins `INDEXED` in the index and in the `PLUGIN-39` gate, and
599
+ `--triggers` emits ONE row per stack rather than one per file, because the three files of a
600
+ stack are applied together and share one `detect:`.
601
+ - **Routing frontmatter for five document families.** `features` gain `status`, `for_paths`,
602
+ `us`; `adr` gains `status`, `trigger_path`, `superseded_by`; `research` and `protocols` gain
603
+ `fires_on`/`fires_when`, plus `consumed_by` for a dossier something already used; `evolution`
604
+ gains `status` only. `--triggers` emits them and `--for` matches through `for_paths` and
605
+ `trigger_path`. Before this, `afm-index.sh --for plugins/afm/hooks/afm-pre-edit.sh` returned
606
+ nothing while a feature spec, an ADR and a research dossier all governed that file.
607
+
608
+ ### Changed
609
+
610
+ - **`afm-pre-edit.sh` deduplicates by CONTENT, not only by file.** A gotcha, rule or policy
611
+ body already delivered in this session emits a one-line pointer instead of the body. The key
612
+ is a hash of the emitted text, never `path+mtime` — `git checkout`, stash and rebase reset
613
+ mtime without changing content, and that key would suppress a body that is genuinely
614
+ different. It stays a pointer rather than going silent, because a mechanism that fails by
615
+ silence is this project's most expensive defect: the gotcha is relevant again 300 turns
616
+ later, when the body has scrolled out of attention. Rubric rows and the legacy monolith
617
+ branches are deliberately out of scope.
618
+ - **`SessionStart` no longer re-injects on resume.** It fired on `startup`, `resume` and
619
+ `compact` — 1.43 injections per session against 0.88 before — re-sending about 1141 tokens
620
+ already resident. The matcher now reads `startup|clear|compact` and the script itself
621
+ branches on the payload's `source`. Both layers, deliberately: `test/run.sh` invokes the
622
+ scripts directly and never through `hooks.json`, so a matcher-only fix would ship with no
623
+ coverage. `compact` keeps injecting — the summary may not carry the doctrine.
624
+
625
+ ### Fixed
626
+
627
+ - **`interview/bootstrap.md` named six stack modules while seven shipped.** `vireya-ui` existed
628
+ in the plugin and no bootstrap could ever select it, because the prose that listed the modules
629
+ was not the directory that held them. The step now reads the index instead of a hand-written
630
+ list.
631
+ - **Hard rule `PLUGIN-36` failed the tree over its own evidence.** It excluded `docs/sessions/`
632
+ as a machine-written append-only record and missed the two siblings, `docs/.afm-log/` and
633
+ `docs/.afm-log-failures/`. On 2026-09-03 the CRITIC hook recorded a failing command verbatim —
634
+ the point of the record — and the absolute path inside that command failed the gate. Rewriting
635
+ the record to satisfy the rule would destroy what the record exists to hold, and rules 17/18
636
+ forbid rewriting either substrate.
637
+ - **A Portuguese write to a code file burned that file's rule injection for the whole
638
+ session.** The per-file flag was written unconditionally, then `LANG_EMITTED` discarded the
639
+ whole payload, so every later edit of that file — English ones included — hit the cache and
640
+ received nothing. The flag now means "delivered", not "attempted". The regression assert is
641
+ the one that proves it: Portuguese write, then English write of the same file, must deliver.
642
+
643
+ ### Consumer migration
644
+
645
+ - **Consumer-facing.** `CONSUMER_VERSION` moves to `3.31.0`. Two deltas reach a materialized
646
+ instance: the five templates now seed the routing keys so a new document is born routable,
647
+ and `docs/tools/` gains `afm-token-audit.sh` where the instance materializes tools. The new
648
+ frontmatter keys are **optional** in the `PLUGIN-39` gate — making them required would break
649
+ every instance pinned below 3.30.0 in one release. An instance may run `/afm:update` and
650
+ backfill its own documents when it chooses.
651
+
652
+ ## [3.30.0] — 2026-08-30
653
+
654
+ Closes the AFM 4.0 line. Seven of its ten phases had shipped, one was cancelled by its own
655
+ evidence, and what remained turned out not to be a major. Feature
656
+ `docs/features/010-close-the-v4-line/`.
657
+
658
+ ### Why this is 3.30.0 and not 4.0.0
659
+
660
+ `4.0.0` was a major because of F4 — consolidating and removing operations, the one change in this
661
+ plugin that breaks a materialized instance. `docs/adr/0012-*` removed that input with 100 eval
662
+ trials: all seven zero-usage operations score 5/5 on sonnet, so none is removed. What was left
663
+ renames nothing, removes nothing, and breaks no reference. A version number describes what a
664
+ release does to whoever depends on it, and this one adds a hook.
665
+
666
+ ### Added
667
+
668
+ - **`hooks/afm-capture-op.sh` — the ledger records WHICH OPERATION ran.** A `PostToolUse` hook on
669
+ matcher `Skill` appends `- [ISO] op | s=<sid6> | afm:<name>`.
670
+
671
+ It exists because ADR-0012 stated its own limit in the same breath as its verdict: a synthetic
672
+ eval measures **routability**, never **organic frequency**, and merging two operations depends on
673
+ the second question. The ADR named the event ledger as the only source that could answer it.
674
+ Measured on 2026-08-30, that ledger held **659 events** and `grep 'afm:'` across all of them
675
+ returned **zero** — it recorded the artifact written, never the operation that ran. The clock the
676
+ ADR said to wait for had never been wound.
677
+
678
+ Three properties are load-bearing. It records **`afm:*` only** — counting another plugin's skill
679
+ would poison the ratio the event exists to produce. It records the **invocation, not the
680
+ outcome** — an operation that ran and failed still answers "was it reached". And `op` joins the
681
+ ledger's closed vocabulary as a **telemetry** kind, so `reconcile` filters it out alongside
682
+ `task`/`deliverable`/`failure`. **No reader ships with it:** a counter over four events would
683
+ invite exactly the conclusion ADR-0012 warned against.
684
+
685
+ The design turned on one unknown — does `PostToolUse` fire for the `Skill` tool? It was probed
686
+ with a throwaway plugin and `claude -p --model haiku` before a line of the plan depended on it
687
+ (#2). It fires, and `tool_input.skill` carries the qualified name.
688
+
689
+ ### Changed
690
+
691
+ - **`CLAUDE.md` of this repository: 26,878 → 6,850 characters.** Criterion #5 of the v4 intent said
692
+ the resident cost must not grow. The skill listing obeyed it (14,182 → 10,264 when `when_to_use`
693
+ was merged away); this file went the other way and became the largest resident block in the
694
+ project, larger on its own than the listing of all 25 skills. The criterion applied was
695
+ duplication, never a character target: every block that `/docs` already held became a one-line
696
+ pointer, and what has no other home — the machine-local install state, the credential map, the
697
+ `{{placeholder}}`/`[TBD]` conventions, the three asymmetries that look like drift — stayed. One
698
+ whole section described `.creator/`, a directory that no longer exists.
699
+
700
+ - **The 80% prune, on the four densest files** (`core/afm.md`, `ops/{deliver,evolve,update}.md`).
701
+ Yield: **2,254 characters of about 141,000 — 1.6%** — and the number refutes the premise the v4
702
+ intent wrote the prune on. Those files are not full of generic prose; measured block by block,
703
+ almost everything carries a number, a counter-example or a paid bug, which the plan's own
704
+ guardrail forbids cutting. What did come out was **rule-34 drift**, not verbosity: `deliver.md`
705
+ § 3.B.1 restated the whole `core/afm.md § 2` loop, `evolve.md` said "never the plugin" three
706
+ times, and `core/afm.md § 1.2` restated the Pragmatic-Programmer canon whose two load-bearing
707
+ halves already lived at their point of use.
708
+
709
+ ### Fixed
710
+
711
+ - **Two dead cross-references inside hard rules' own verification lines.** `PLUGIN-1` and
712
+ `PLUGIN-21` handed their judgment half to "§ 5.1" — the GREEN-fails retry loop nested under § 2 —
713
+ when the section they meant is § 5, *Signs you are off-track*. Same family as `PLUGIN-40`: the
714
+ writer moved and the reader did not.
715
+
716
+ - **`docs/adr/0012-*` cited a path no clone had.** The two v4 dossiers sat at the repository root,
717
+ uncommitted and outside `docs/`, where no rule covered them. They are now
718
+ `docs/research/008-afm-v4-intent.md` and `009-afm-v4-phased-plan.md`, with the OKF frontmatter,
719
+ and the citation resolves.
720
+
721
+ ### Consumer migration
722
+
723
+ - **Consumer-noop.** Nothing materialized into `<consumer>/docs/` changes, so `CONSUMER_VERSION`
724
+ stays where it is and no instance needs `/afm:update`. The new hook lives in
725
+ `plugins/afm/hooks/`, which travels with the plugin install and not with the instance: a
726
+ consumer that updates the plugin gets the `op` event on its next session, and one that does not
727
+ keeps working exactly as before with the ledger it already had. Nothing is renamed, nothing is
728
+ removed, and no reference changes.
729
+
730
+ ## [3.22.0] — 2026-08-30
731
+
732
+ Closes the known gaps that were open work rather than defects. Feature
733
+ `docs/features/009-app-facing-language/` plus the reconciliation of feature 003.
734
+
735
+ ### Added
736
+
737
+ - **`prd.md § 11.1` — the app-facing string language.** Hard rule `PLUGIN-37` says the prose of an
738
+ artifact is English, and it has exactly one exception: the strings a product shows its own users
739
+ are a product decision. Measured on 2026-08-30, that exception existed on **no surface a machine
740
+ can read** — `grep` across `core/prd.md`, both interviews, `triggers.md` and `ops/update.md`
741
+ returned zero.
742
+
743
+ The gap has a direction. A language migration could not tell an artifact from a product string,
744
+ so it would translate the UI, and `git revert` does not give a user back the screen they saw.
745
+ See `docs/adr/0022-*`.
746
+
747
+ - **`ops/update.md § 1.6` — the language migration step.** The order is normative: read
748
+ `prd.md § 11.1`, derive the excluded scope from it, and only then read a file. Scan-then-skip
749
+ makes the exclusion depend on the agent noticing during the work. `[TBD]` or an absent section
750
+ stops the step and asks. It never picks a language.
751
+
752
+ - **The interview asks it.** One closed question inside `bootstrap.md` step 0 (recommendation-first,
753
+ `[TBD]` accepted) and `refactor.md` C.9.3b, which scans the UI strings the code already carries
754
+ and offers what it found first (#2).
755
+
756
+ - **`triggers.md` — a `reconcile` signal** for an artifact written in another language. It reads
757
+ § 11.1 **before** it fires, so it can separate a violation from the declared exception.
758
+
759
+ ### Fixed
760
+
761
+ - **`afm-health.sh` A2 over-counted markers.** The filter excluded a backtick immediately before
762
+ the marker, which only catches `` `[TBD]` `` written alone. It missed every longer code span, so
763
+ `` `grep [NEEDS CLARIFICATION:]` `` and `` `protocol: [TBD]` `` counted as open gaps. On
764
+ 2026-08-30 the hook reported 5 open markers and all five were docs quoting the notation. The
765
+ filter now strips inline code spans before it counts. A counter that reports work nobody has to
766
+ do is read once and then ignored, which costs the same as reporting zero.
767
+
768
+ - **Three ops named the OLD Portuguese guardrail frontmatter keys.** `rule.md` and `remediate.md`
769
+ still instructed the writer to fill `origem:`, `superficie` and `custo`, renamed to `origin:`,
770
+ `surface:` and `cost:` in the schema rename. The writers moved and these readers did not — the
771
+ same shape the 3.20.0 and 3.21.0 defects had.
772
+
773
+ - **29 `block` findings in `ste-lint --scope strict`** across 7 op files (26 S1, 2 S4, 1 S3), the
774
+ acceptance check of feature 003 phase B that had never been run. Now 0 across all 48 skills and
775
+ ops.
776
+
777
+ ### Changed
778
+
779
+ - **Six ops link to `PRINCIPLES.md`** where they cite an invariant principle. This clears the
780
+ `34 ops` ratchet row, carried since 2026-08-22.
781
+
782
+ ### Consumer migration
783
+
784
+ - **`prd.md` gains § 11.1, "App-facing string language".** Add it after § 11 with the value
785
+ `[TBD]` unless you already know the answer. An instance without the section is treated exactly
786
+ like `[TBD]`: the migration asks instead of assuming.
787
+ - Nothing else in a materialized instance changes. The interview, `triggers.md` and
788
+ `ops/update.md` are plugin surfaces.
789
+
790
+ ## [3.21.0] — 2026-08-30
791
+
792
+ The first full `/afm:evolve` cycle this instance has ever run. Report:
793
+ `docs/evolution/2026-08-30-005.md`.
794
+
795
+ ### Added
796
+
797
+ - **Hard rule `PLUGIN-40` — a normative doc cites no path that does not exist**, with its runner
798
+ in `tools/gate/core-rules/`. It promotes the `diagnose` A1 axis from a SessionStart report into
799
+ a commit-time refusal. The axis had detected the defect that same day and reported it; the
800
+ report was read and the work continued. Detection at the start of a session and refusal at the
801
+ commit are not the same event, and only the second arrives while the change is still in the
802
+ hands of whoever made it.
803
+
804
+ The scope is the normative families only — `afm.md`, `ach.md`, `prd.md`, `ust.md`, `_focus.md`,
805
+ `adr/`, `rules/`, `gotchas/`, `learnings/`, `guardrails/`, `procedures/`. Pointed at every doc
806
+ it produced 44 findings that were all correct notation: a rubric teaches with an illustrative
807
+ path, a research dossier quotes a hypothetical one, a feature's `tasks.md` names paths as they
808
+ were. A gate that starts red is switched off the next day.
809
+
810
+ A cited path resolves **from the repository root OR from the directory of the file that cites
811
+ it**. Both conventions are alive in a real instance, and a checker that knows only one invents
812
+ findings — the same end as a gate that finds nothing, reached from the other side.
813
+
814
+ ### Fixed
815
+
816
+ - **Seven rule files named a `*Verification:*` target that had just moved.** 3.20.0 updated the
817
+ link and left the visible label saying `test/rules/<n>-*.sh`. The reader was sent to a file
818
+ that does not exist. The same rename defect the release was about, one layer down.
819
+ - **`PLUGIN-37` was invisible to the phantom-gate check.** Its header read
820
+ `*Verification (two moments, one measurement):*`, which is not a surface keyword, so
821
+ `rule_surface` answered `agent` — for that rule and no other. Deleting its runner would have
822
+ left the audit green.
823
+ - `CLAUDE.md` still said "hard rule 37" after the id became `PLUGIN-37`.
824
+
825
+ ### Documentation
826
+
827
+ - `docs/learnings/uma-asercao-de-regressao-vale-contra-o-codigo-antigo.md` — a regression
828
+ assertion is worth nothing until it has been run against the OLD code and gone red. Written
829
+ after two fixtures went green against the broken code on 2026-08-30, both because they used the
830
+ new file-name shape against the old parser. Injected by path match on `test/**`.
831
+
832
+ ### Consumer migration
833
+
834
+ `/afm:update` copies the `PLUGIN-40` runner and materializes the rule into `afm.md § 3`. Run
835
+ `guardrails.sh --baseline` afterwards: an instance with historical citations starts with declared
836
+ debt instead of a red gate. Three such rows exist in this repository, each with its reason — two
837
+ ADRs and one user story that quote a path **because** it stopped existing.
838
+
839
+ ## [3.20.0] — 2026-08-30
840
+
841
+ The plugin shipped rules it never shipped runners for. A consumer's `afm.md` stopped at rule 21.
842
+
843
+ The version jumps from 3.13 to 3.20 on the owner's call: the change is large, and 4.0.0 is
844
+ reserved for the v4 line.
845
+
846
+ ### Added
847
+
848
+ - **`tools/gate/core-rules/` — seven runners that travel with the plugin.** `PLUGIN-13` secrets,
849
+ `PLUGIN-21` code language, `PLUGIN-35` gate completeness, `PLUGIN-36` absolute path,
850
+ `PLUGIN-37` prose language, `PLUGIN-38` parser marker, `PLUGIN-39` OKF frontmatter. They
851
+ enforce the contract of the `docs/` AFM materializes, which is not something a consumer
852
+ chooses. `core/afm.md § 3` gains the text of `PLUGIN-35` to `PLUGIN-39`; until now those five
853
+ rules existed only in the plugin's own repository. `PLUGIN-35` was the sharpest case: it
854
+ demands a versioned runner behind every 0/1 trigger, and it shipped as prose with no runner.
855
+ - **`ratchet_exempt`** in `gate/lib/ratchet.sh`. An exemption is a row whose count column is the
856
+ literal `exempt`, not a budget of zero. It moves a path exclusion out of a `case` arm inside a
857
+ runner — where only one repository could have it, and no reviewer would see it — into a
858
+ versioned table.
859
+ - `--core-rules` on the aggregator, plus `AFM_GATE_CORE_RULES` and `AFM_GATE_REPO`.
860
+
861
+ ### Changed
862
+
863
+ - **Two namespaces replace the reserved number range** (ADR 0021). `PLUGIN-<n>` is a rule the
864
+ plugin ships; a plain `<n>` is a rule this project wrote about its own code; 100+ stays the
865
+ organisation policy. `core/afm.md` used to reserve 18-29 and tell a project to start at 30 — a
866
+ promise about numbers that was already broken, since this repository had spent 35-39 on rules
867
+ that turned out to be universal. Seven parsers now read `(PLUGIN-)?<n>`: the aggregator's
868
+ `rule_number`, `rules_declared` and `rule_surface`, `afm-session-start.sh`, `afm-pre-edit.sh`,
869
+ `afm-migrate-okf.sh` and `afm-index.sh`.
870
+ - `gate/rules/README.md` no longer says the directory is empty on purpose without qualification.
871
+ It names the two classes and points at `core-rules/`.
872
+ - `interview/{bootstrap,refactor}.md` and `ops/update.md` materialize `core-rules/` and the
873
+ `PLUGIN-` renaming of the inherited rules.
874
+
875
+ ### Fixed
876
+
877
+ Four defects that only appeared once the shipped runners actually ran. Each had been reporting
878
+ green.
879
+
880
+ - **The runner glob `[0-9]*.sh` never matched a namespaced name.** The whole `core-rules/`
881
+ directory was invisible to the aggregator, and a glob that matches nothing reports nothing.
882
+ - **`rule_surface` had read only `afm.md` since the 3.9.1 split**, where § 3 became an index and
883
+ the `*Verification*` lines moved into `rules/*.md`. It answered `agent` for every rule, so the
884
+ phantom-gate branch could never fire. Deleting a runner produced no finding at all — from the
885
+ one check whose purpose is to catch a rule that promises a gate nobody runs. It now reads both
886
+ layouts, inline first.
887
+ - **A nested aggregator call re-resolved the rule directories from the defaults.** `PLUGIN-35`
888
+ invokes the aggregator, and the nested run reported every shipped rule as a phantom gate — a
889
+ failure invented by the check itself.
890
+ - **An `exempt` row acted as a region with a budget of zero**, so it absorbed every finding under
891
+ that path and failed there instead of skipping it. The exemption became its opposite.
892
+
893
+ Also: every runner derived the repository as `$(cd "$(dirname "$0")/../.." && pwd)`, correct only
894
+ while it sat two levels below the root. Five levels down that pointed at `tools/`, and
895
+ `PLUGIN-37` reported "cannot be decided" against a file that was right there. They read
896
+ `AFM_GATE_REPO` now, and `PLUGIN-35` resolves the aggregator through `$0` instead of a repository
897
+ path.
898
+
899
+ ### Consumer migration
900
+
901
+ Run `/afm:update`. It copies `gate/core-rules/`, materializes `PLUGIN-35` to `PLUGIN-39` into
902
+ `afm.md § 3`, and renames the inherited rules into the namespace — the number in the index, the
903
+ `rules/<n>-*.md` filename, the `rule_number:` frontmatter and the ratchet `rule` column move
904
+ together. An instance that has already written its own rule 35 keeps it: the namespaces do not
905
+ collide.
906
+
907
+ ## [3.13.0] — 2026-08-30
908
+
909
+ `/afm:evolve` had never run once. Three defects, and each one alone was enough.
910
+
911
+ The owner named the diagnosis correctly: a mechanism that never fires is not a fact about
912
+ one repository, it is a defect in the plugin. All three were measured in the plugin's own
913
+ instance before anything was changed.
914
+
915
+ ### Fixed
916
+
917
+ - **The capture substrate was blind to an edit made with Bash.** `afm-capture.sh` handled
918
+ `tool_name == "Bash"` by looking for `git commit` and then exiting, unconditionally, before
919
+ ever reaching the branch that classifies `tool_input.file_path`. Every document written with
920
+ `sed -i`, a heredoc, `tee`, `cp` or `mv` was invisible. The cost was not a missing log line:
921
+ the `deliverable` transition that gates the whole cadence is emitted from that branch.
922
+ Measured: features 006, 007 and 008 closed 80 tasks and produced **zero** `feature-tasks`
923
+ events, and the ledger held **2** `deliverable` events across **7** closed features.
924
+ The classification is now one function with two callers (rule 34). The Bash branch extracts
925
+ write TARGETS — anchored on a write verb, then filtered to paths under `docs/` that exist on
926
+ disk — so `grep -rn foo docs/afm.md 2>/dev/null` still emits nothing.
927
+
928
+ - **`reflect` starved `evolve` through a shared cadence window.** Both write into
929
+ `docs/evolution/`, and the counter read "the newest file in that directory" as "the last
930
+ cycle" for both. `reflect` runs every N tasks and drops a report there; `evolve` needs N
931
+ deliverables accumulated *after* the last cycle. So every `reflect` run reset the `evolve`
932
+ window, and the threshold was unreachable by construction. The disk showed the exact
933
+ outcome: five reports, all five from `reflect`, `evolve` never. Each report now declares
934
+ `mechanism:`, and `afm_count_since <kind> [mechanism]` scopes the window to that mechanism's
935
+ own last report.
936
+
937
+ - **The report selector ordered by ordinal before date.** The name was `NNN-YYYY-MM-DD.md` and
938
+ the selector was `sort | tail -1`. Two `001-*.md` already existed, so a low ordinal on a
939
+ recent date rewound the window in silence. The name is now `YYYY-MM-DD-NNN.md`, the ordinal
940
+ is only a same-day tiebreak, and the reader accepts both shapes during migration.
941
+
942
+ - **A section title in the hooks smoke test ran a command.** `sec "… swept into \`doc\` =="`
943
+ put backticks inside a double-quoted string, so the harness executed `doc` and printed
944
+ `command not found` while the title came out blank. It never failed a test, which is why it
945
+ survived.
946
+
947
+ ### Added
948
+
949
+ - `tools/afm-migrate-okf.sh --evolution` — the structural migration. It renames to date-first,
950
+ reads the mechanism out of the report's own prose, and **refuses to guess**: a report that
951
+ does not say what wrote it is reported and left alone, and the command exits non-zero
952
+ (#5). Live pointers in `docs/` move with the file; `docs/.afm-log/` is append-only (rule 18)
953
+ and is left exactly as it was written.
954
+ - 10 assertions in `plugins/afm/hooks/test/run.sh` (171 → 181), one per defect, with legacy
955
+ `NNN-YYYY-MM-DD` fixtures. The first draft of the D2/D3 fixtures used the **new** name shape
956
+ against the **old** parser and went green against 3.12.2 — proving nothing. That is the
957
+ "verifier that measures the near thing instead of the right thing" family this repo has
958
+ already recorded; the fixtures were rebuilt to be genuinely red.
959
+
960
+ ### Changed
961
+
962
+ - `templates/evolution-report.template.md`, `ops/{reflect,evolve,restructure,generalize}.md`:
963
+ `mechanism:` is mandatory in the report frontmatter, and the name is date-first.
964
+ - `ops/evolve.md`: the cooldown and the resumability check now read the newest report **with
965
+ `mechanism: evolve`**, not the newest file in the directory.
966
+
967
+ ## [3.12.2] — 2026-08-29
968
+
969
+ Feature 008, item 9. English, and the debt that was never going to be paid.
970
+
971
+ ### Changed
972
+
973
+ - **`CLAUDE.md`, `README.md` and `guide/tutorial_afm.md` are English under STE.** `CLAUDE.md` is
974
+ the one that mattered: every agent session in this repository reads it, and it published rule 37
975
+ while breaking it. It was translated sentence by sentence rather than rewritten, so no caveat was
976
+ traded for brevity.
977
+ - **The ratchet learned to say `declared` instead of `debt`.** A note that opens with `PERMANENT:`
978
+ is a declaration, not a debt. Two regions qualify by their nature, not by their size:
979
+ `docs/sessions` is episodic history that only grows, and translating a past session rewrites a
980
+ record. `test/evals` holds fixtures that simulate a consumer's `/docs`, and the assertions match
981
+ their content, so translating without touching the assertion is the failure the fixture exists to
982
+ catch.
983
+
984
+ Carrying those as debt forever made the debt list mean less every time it was read. The count is
985
+ still the ceiling, so a real increase is still caught.
986
+
987
+ - The root region drops from 5 to 2. What remains is `afm_v4_idea.md` and `afm_v4_plan.md`, 16,607
988
+ words of planning dossiers that are not a reading surface. Declared, not hidden.
989
+
990
+ ### Notes
991
+
992
+ - **Consumer-noop.** Nothing a consumer materializes changed. The ratchet convention travels with
993
+ the gate, and an instance with no `PERMANENT:` note behaves exactly as before.
994
+
995
+ ## [3.12.1] — 2026-08-29
996
+
997
+ Feature 008, item 8. The cadence, and the defect that made its number fiction.
998
+
999
+ ### Fixed
1000
+
1001
+ - **A GENERATED file poisoned the cadence counter.** `afm_count_since` picks the last evolution
1002
+ cycle with `ls docs/evolution/*.md | sort | tail -1`. Since 3.9.1 an `index.md` is generated into
1003
+ every directory, and `index.md` sorts after `004-2026-08-25.md`. The date extraction then yields
1004
+ nothing, and the counter silently switches from "since the last cycle" to "since the beginning of
1005
+ time".
1006
+
1007
+ **Measured: it reported 222 tasks overdue against a real 150.** That number was read as "the
1008
+ cadence has been ignored 55 times" and nearly bought a threshold change on its own.
1009
+
1010
+ - **Three more globs had the same poisoning**, all introduced by the generated index: axis A7
1011
+ counted the indexes into the doc-to-code ratio, the rule-trigger extractor read `rules/index.md`,
1012
+ and this repository's own rehearsal counted protocol presets. `index.md` and `log.md` are OKF
1013
+ RESERVED names (SPEC v0.2 §3.1), never concept documents, and every glob that assumes otherwise
1014
+ is wrong the same way.
1015
+
1016
+ ### Changed
1017
+
1018
+ - **The reflection cadence threshold is 20, and the measurement is beside it.** Tasks closed per
1019
+ session in this repository: **8, 17, 18, 29 and 150 — a median of 18.** The old default of 4 came
1020
+ due four times inside a median session, and G2 caps the mechanism at ONE block per session, so
1021
+ the threshold was saturated by construction. It blocked every session that did any work, which
1022
+ reads as noise and gets ignored.
1023
+
1024
+ 20 is one median session of work. Change it when the pace changes, and put the new measurement
1025
+ next to it — a threshold with nothing behind it is what this replaced.
1026
+
1027
+ - **The threshold lived in four places**, and only two of them had been found before: the key
1028
+ default, the reader's initial value, the out-of-range clamp, and the fallback in each of the two
1029
+ hooks that read it. Same shape as the `core.hooksPath` misread of 3.11.4. When a value has many
1030
+ readers, changing one is changing none.
1031
+
1032
+ ### Notes
1033
+
1034
+ - `hooks/test/run.sh`: 170 → 171 assertions, including one that proves a generated index cannot be
1035
+ mistaken for the last cycle.
1036
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.12.1).** An instance that materialized indexes
1037
+ has an under-counting cadence. After this update its counter reads the real number, which may be
1038
+ higher than what it was reporting.
1039
+
1040
+ ## [3.12.0] — 2026-08-29
1041
+
1042
+ Feature 008, item 6. The structural migration has a rehearsal.
1043
+
1044
+ ### Added
1045
+
1046
+ - **`test/update-rehearsal.sh` rehearses the 3.9.1 structural migration** on a second, disposable
1047
+ consumer built in the pre-3.9.1 shape. The fixture carries the shapes that broke the tool: a
1048
+ heading with a colon, one with a double quote, an entry with no `**Trigger:**` line, rule bodies
1049
+ inside section 3, and `kind:` frontmatter.
1050
+
1051
+ It runs the documented sequence from `ops/update.md` § Structural migrations and asserts the four
1052
+ failures this migration actually produced. **A rehearsal that asserts success proves nothing.**
1053
+
1054
+ | Assert | The defect it catches |
1055
+ | --- | --- |
1056
+ | no entry is lost, in or out | the count guard the script claims |
1057
+ | no rule body survives in section 3 | 3.11.3 — the split left every rule in two places |
1058
+ | every frontmatter parses as YAML | 3.10.1 — a colon in a heading made a file unreadable |
1059
+ | `--check-index` returns 0 | rule 39 fails on a missing index, so a skipped `--materialize` lands the operator red |
1060
+
1061
+ **Both were reintroduced on purpose and both turned it red.** An assertion never seen failing is
1062
+ an assertion nobody has tested — which is how `--check-index` spent hours verifying itself.
1063
+
1064
+ - `ops/update.md` says the rehearsal exists and what it answers. A unit test in `tools/test/run.sh`
1065
+ asks whether one command behaves. The rehearsal asks whether an operator who follows the page
1066
+ ends with a correct instance.
1067
+
1068
+ ### Notes
1069
+
1070
+ - The rehearsal went from 11 to 22 assertions.
1071
+ - Writing it, the repository's own gotcha 004 bit: a backtick inside a double-quoted string became
1072
+ command substitution. The gotcha exists, and it still catches the person who wrote it down.
1073
+ - **Consumer-noop.** No file a consumer materializes changed.
1074
+
1075
+ ## [3.11.6] — 2026-08-29
1076
+
1077
+ ### Fixed
1078
+
1079
+ - **`ops/evolve.md` said five mechanisms and its `composes:` listed six.** Both were right, and the
1080
+ fix was not a number. `diagnose`, `reflect`, `generalize`, `restructure` and `remediate` are the
1081
+ mechanisms. `update` is composed too, on axis A3, and it is not a mechanism. The file now states
1082
+ the distinction so the next reader does not "correct" either count into the other.
1083
+
1084
+ Rule 39 caught the first attempt at that fix: the new `description` carried a colon and stopped
1085
+ being parseable YAML, which is the exact defect 3.10.1 removed, reintroduced an hour later by the
1086
+ person who removed it. The gate working is the answer to that, and it is why it exists.
1087
+
1088
+ ### Added
1089
+
1090
+ - **`docs/research/007-afm-next-steps-by-roi.md`** — the backlog ranked by return on investment,
1091
+ with the scoring model written before any number arrived. Fifteen candidates, sized by agents
1092
+ that had to run the check rather than read the changelog. Four dropped, three of them correcting
1093
+ a premise of the person who proposed them.
1094
+
1095
+ ### Notes
1096
+
1097
+ - **Consumer-noop.**
1098
+
1099
+ ## [3.11.5] — 2026-08-29
1100
+
1101
+ Three more readers that the 3.9.1 split left behind, and one of them had been silently wrong for
1102
+ two months.
1103
+
1104
+ ### Fixed
1105
+
1106
+ - **The capture substrate stopped seeing gotchas on 2026-08-29.** `afm-capture.sh` classified by
1107
+ path and its table still named `docs/gotchas.md`, so every gotcha written after the split was
1108
+ recorded as a generic `doc`. **Measured: 42 `gotcha` events in the ledger, the newest dated
1109
+ 2026-08-28, against 28 gotcha files on disk.** A rule written under `docs/rules/` had the same
1110
+ problem.
1111
+
1112
+ This is not a wrong label. The ledger is what the self-evolution loop counts: `generalize` fires
1113
+ on "two or more similar gotchas accumulated", and it was counting zero. A misclassification here
1114
+ makes a mechanism blind, and a blind mechanism reports nothing to fix.
1115
+
1116
+ - **`afm-subagent-start.sh` pointed a sub-agent at the deleted monolith.** It checked
1117
+ `[ -f ./docs/gotchas.md ]`, which is false since the split, so the sub-agent was told nothing
1118
+ about gotchas at all.
1119
+
1120
+ - **`ops/rule.md` step 0 conflated the two reasons `docs/tools/gate/` can be missing.** A project
1121
+ that maintains the plugin skips that copy on purpose (hard rule 34) and its gate lives at
1122
+ `./test/`. Any other project with no such directory is behind the plugin. The op said only the
1123
+ second, so a plugin-maintaining project was told to run `/afm:update` for something correctly
1124
+ absent — and then to write its runner into a directory nothing would read.
1125
+
1126
+ The asymmetry is the point, and it is the inverse of the one fixed in 3.11.2: here the READERS
1127
+ were already right. `afm-session-start.sh`, `afm-health.sh` and `afm-pre-commit.sh` all try
1128
+ `docs/tools/gate` and fall back to `./test/`. The writer was the only one that did not.
1129
+
1130
+ ### Notes
1131
+
1132
+ - `hooks/test/run.sh`: 168 → 170 assertions.
1133
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.5).** An instance that migrated to the split
1134
+ has an event ledger with a gap. The history cannot be rebuilt — the events were written with the
1135
+ wrong kind and rule 18 makes the substrate append-only — but the gap ends here.
1136
+
1137
+ ## [3.11.4] — 2026-08-29
1138
+
1139
+ ### Fixed
1140
+
1141
+ - **The same absolute-`core.hooksPath` bug lived in `afm-pre-commit.sh`.** 3.11.1 fixed it in the
1142
+ health axis. Here the consequence is different: the hook decided it was the only gate surface
1143
+ while git was already running the same gate through the native hook, so **the gate ran twice per
1144
+ commit** — and the refusal message told the operator they had no native surface while they were
1145
+ reading its output.
1146
+
1147
+ One bug, two hooks, found a day apart because the first symptom was a false alarm and the second
1148
+ was a doubled cost. Both directions are now covered in `hooks/test/run.sh`: an absolute path
1149
+ makes the tier-3 hook yield, and an unset one keeps it denying a red gate.
1150
+
1151
+ ### Notes
1152
+
1153
+ - Worth carrying: **when a defect is a misread of a config value, grep for every reader of that
1154
+ value.** `git config --get core.hooksPath` had two readers and both were wrong the same way.
1155
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.4).** Any instance whose operator set an
1156
+ absolute `core.hooksPath` was paying for the gate twice on every commit.
1157
+
1158
+ ## [3.11.3] — 2026-08-29
1159
+
1160
+ ### Fixed — the rules split only did half the job
1161
+
1162
+ `afm-migrate-okf.sh --rules` copied every rule body into `docs/rules/NN-slug.md` and left section 3
1163
+ of `afm.md` exactly as it was. A consumer who followed the documented sequence ended with **every
1164
+ rule in two places**, and the duplicate is the dangerous half: the two copies drift and the reader
1165
+ has no way to tell which one is current.
1166
+
1167
+ This repository's own section 3 was rewritten **by hand** after the split, and the procedure
1168
+ documented no such step. A tool has to do what the maintainer did, or the maintainer's result is
1169
+ not reproducible.
1170
+
1171
+ The same command now splices section 3 into the index, preserving three things a naive rewrite
1172
+ would lose: the section intro, section 3.1, and the exact `N. **title**` shape that
1173
+ `afm-session-start.sh` and `afm-health.sh` parse. It warns when an indented `*Verification*` line
1174
+ survives, which means something in the source did not match the expected shape.
1175
+
1176
+ - `ops/rule.md` writes **both halves** now: the file under `rules/` and the index line in section
1177
+ 3. It says why the second is not optional — a rule written only as a file never reaches a
1178
+ session, and a rule written only as an index line has no body.
1179
+ - `tools/test/run.sh`: 60 → 65 assertions. Four of the five check what a naive splice destroys.
1180
+
1181
+ ### Notes
1182
+
1183
+ - Found by an adversarial sizing pass that was asked to actually RUN the documented update
1184
+ sequence on a fixture consumer instead of reading it.
1185
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.3).** An instance that already ran `--rules`
1186
+ between 3.9.1 and 3.11.2 has duplicated bodies. Re-running is not the fix — section 3 no longer
1187
+ matches the pattern. Delete the numbered bodies from section 3 by hand and keep the index lines.
1188
+
1189
+ ## [3.11.2] — 2026-08-29
1190
+
1191
+ ### Fixed — the ops still wrote to the file 3.9.1 deleted
1192
+
1193
+ Found while ranking the backlog by return on investment, and it outranked everything on the list.
1194
+
1195
+ `/afm:gotcha` declared `writes: ["docs/gotchas.md"]` in its frontmatter and instructed *"append to
1196
+ `<project>/docs/gotchas.md`"*. That file has not existed since 3.9.1. A gotcha registered today
1197
+ would have **recreated the monolith beside the directory**, where `afm-pre-edit.sh` — which prefers
1198
+ `docs/gotchas/` — would never look at it. A gotcha written and never delivered, which is the exact
1199
+ failure feature 006 existed to remove, reintroduced by the operation that creates them.
1200
+
1201
+ `afm-index.sh --writes docs/gotchas.md` answered `gotcha` and `generalize`. Asking who writes
1202
+ `docs/gotchas/001-x.md` answered only `verify`, through an unrelated glob.
1203
+
1204
+ - **Three output contracts corrected**: `gotcha` → `docs/gotchas/*.md`, `generalize` → gotchas and
1205
+ rules as directories, `rule` → `docs/rules/*.md` alongside `afm.md`.
1206
+ - **`ops/gotcha.md` now writes one file with its frontmatter**, and says why the frontmatter is the
1207
+ delivery mechanism rather than decoration: the detector reads `trigger_basename`, `trigger_path`
1208
+ and `trigger_grep` and nothing else, so an entry without one never reaches anybody. It repeats
1209
+ the measured lesson — prefer an honest `event` over a guessed `file-shape`, because 17 of the 26
1210
+ gotchas in this repository are events and the old text match fired them on coincidence.
1211
+ - **Eleven stale READS swept.** This is the quieter half: `grep ./docs/gotchas.md` against a file
1212
+ that does not exist returns nothing and reports nothing. The `reconcile` duplicate check would
1213
+ never find a duplicate. The `deliver` discovery step would find zero gotchas in the area, and it
1214
+ was still grepping the pre-3.6.0 Portuguese field name. `diagnose` axis A1 covered nothing.
1215
+ - **Universal rule 19's verification command** grepped the monolith, so a failure remediated inside
1216
+ a gotcha reported `NO REMEDIATION`. It now reads `gotchas/` and `rules/`.
1217
+ - **`core/gotchas.md` is an orphan** — bootstrap stopped materializing it in 3.9.1. It carries a
1218
+ tombstone and is **proposed for removal** rather than deleted, because deleting a canonical file
1219
+ is a bright line that stops and asks (PRINCIPLES #7).
1220
+ - `templates/gotcha.template.md` renders to `docs/gotchas/*.md`, and its auto-generated description
1221
+ is written properly.
1222
+
1223
+ ### Notes
1224
+
1225
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.2).** Any instance on 3.9.1 through 3.11.1 has
1226
+ an operation that would write to the wrong path. This is the release that makes `/afm:gotcha`
1227
+ work again after the split.
1228
+ - The general shape: **a rename is finished when the WRITERS and the READERS both move.** 3.9.1
1229
+ moved the files and the hooks and left the operations behind, and nothing failed loudly, because
1230
+ a read against a missing file is silent by construction.
1231
+
1232
+ ## [3.11.1] — 2026-08-29
1233
+
1234
+ ### Fixed
1235
+
1236
+ - **The vital sign called a correctly armed gate disarmed.** `core.hooksPath` can be absolute —
1237
+ `git config core.hooksPath "$PWD/.githooks"` is a common way to set it — and the axis prefixed
1238
+ `./` to whatever it read, producing `.//home/...`, which never exists. This repository has had
1239
+ the gate armed since 3.7.0 and was told every session that it was not.
1240
+
1241
+ A vital sign that cries wolf is worse than a missing one. The operator learns to skip the line,
1242
+ and it takes the true signals in the same bullet down with it. Covered in both directions by
1243
+ `hooks/test/run.sh`: an absolute path is read correctly, and an unset one still reports disarmed.
1244
+
1245
+ ### Notes
1246
+
1247
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.1).** Any instance whose operator set an
1248
+ absolute `core.hooksPath` was seeing the false alarm.
1249
+
1250
+ ## [3.11.0] — 2026-08-29
1251
+
1252
+ The deferred question from 3.10.0 is answered, and the answer removes code instead of adding it.
1253
+
1254
+ ### Changed
1255
+
1256
+ - **Nothing in a policy bundle is ever executed.** The bundle injects CONTEXT. Its rules and
1257
+ gotchas reach the session as text, named and marked as not editable here, and no file in it is
1258
+ run. ADR 0019 is revised with the decision and its reason, which this repository had already
1259
+ written down in `ops/rule.md`: *a repository cannot declare its own hooks, because if it could,
1260
+ `git clone` would be remote code execution.* An environment variable pointing at a shared mount
1261
+ is the same shape one level up.
1262
+
1263
+ ### Removed
1264
+
1265
+ - **`blocking: claim` and `policy_blocking: accept`, and `afm_policy_blocking` with them.** They
1266
+ existed to make execution safe. With execution decided against, they decided nothing — and a
1267
+ field that decides nothing is the phantom gate this repository refuses everywhere else, because
1268
+ it buys trust it cannot back up. They lasted one release and no instance carried them.
1269
+
1270
+ ### Added
1271
+
1272
+ - **A bundle that anyone can write is REFUSED**, and so is one owned by neither this user nor
1273
+ root. Every reader refuses it silently and the health check is the one place that says why. No
1274
+ attacker is assumed: the ordinary mistake is `AFM_GLOBAL_DIR` pointing at `/tmp/something`,
1275
+ where any process on the machine rewrites what every session of every repository is told to
1276
+ follow.
1277
+
1278
+ ### The path to enforcement, unchanged
1279
+
1280
+ An organisation that wants a rule enforced writes a runner into the project's own
1281
+ `docs/tools/gate/rules/`, through `ops/rule.md`. That code is versioned, arrives through a diff,
1282
+ and is reviewed like any other script. The organisation authors it. The project runs it, or does
1283
+ not. A pinned-hash execution from the bundle was considered and rejected: it is safer than
1284
+ executing blindly and still worse than a diff, and re-pinning after an upstream change is exactly
1285
+ the moment nobody reads carefully.
1286
+
1287
+ ### Notes
1288
+
1289
+ - `hooks/test/run.sh`: 159 → 164 assertions, including one that asserts `afm_policy_blocking` does
1290
+ NOT exist. A removal is worth a test when the thing removed was documented.
1291
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.11.0).** An instance that added
1292
+ `policy_blocking` to `docs/afm.md` should delete the key. It never did anything.
1293
+
1294
+ ## [3.10.6] — 2026-08-29
1295
+
1296
+ Three more from the same review. They were filed under "refuted", and five of the seven
1297
+ refutations were worthless: the skeptic's worktree was a stale snapshot, so it answered "cannot
1298
+ reproduce, the file does not exist here". That is not a refutation, and re-checking them by hand
1299
+ found two real defects and one undeclared intent.
1300
+
1301
+ ### Fixed
1302
+
1303
+ - **The pass 0 skip was printed as a FINDING.** The runner contract says stdout carries one line
1304
+ per finding and the aggregator counts them, so `39\t(pass 0)\tSKIPPED` reported a rule 39
1305
+ violation on every machine without the `yaml` module. An audible skip has to be audible, not a
1306
+ false failure. It goes to stderr now, and the gate emits zero stdout lines when it skips.
1307
+ - **A concept file with NO frontmatter escaped pass 0.** Pass 0 checks that a block PARSES and
1308
+ skips a file that has none, which is correct for it — but the families that must have one were
1309
+ only half enumerated. `docs/` itself, `sessions/`, `evolution/`, `research/`, `protocols/`,
1310
+ `measurements/` and `guardrails/` were unchecked, so OKF clause 1 was unenforced for them.
1311
+
1312
+ ### Changed
1313
+
1314
+ - **Rule 39 declares itself a COUNTING rule**, the way rule 36 does. It accepts the mode argument
1315
+ and deliberately ignores it: "is every indexed file readable" is a question about the TREE, and a
1316
+ staged-only answer would call a bundle conformant because this commit happened not to touch the
1317
+ broken file.
1318
+
1319
+ ### Notes
1320
+
1321
+ - The lesson about the review itself is worth more than the three fixes. **A verifier that cannot
1322
+ see the code does not refute anything.** Five of seven refutations here said the file was absent
1323
+ from their checkout. Read the reason, never the verdict.
1324
+ - **Consumer-noop.**
1325
+
1326
+ ## [3.10.5] — 2026-08-29
1327
+
1328
+ An adversarial review of this release line, eight lenses with an independent skeptic per finding,
1329
+ confirmed 14 defects. The nine that were real and not already fixed are below.
1330
+
1331
+ ### Fixed — the migration
1332
+
1333
+ - **A heading with a colon produced frontmatter no YAML parser could read.** `description: Gotcha:
1334
+ the reason` is a nested mapping to YAML. The count check passed and reported success while
1335
+ writing a file no OKF reader can open — the exact silent failure the script's own header claims
1336
+ to prevent.
1337
+ - **A heading with a double quote broke the quoted scalar**, through the `fires_when` fallback that
1338
+ never went through the quote sanitiser.
1339
+ - **A CRLF source carried a carriage return into every field**, and into the visible heading.
1340
+ - All three are one fix: a `yaml_scalar` helper that single-quotes, doubles the inner quote, and
1341
+ strips CR. Single quotes process no escape, so a regex survives them.
1342
+ - **`## 3.1` re-opened section 3.** The pattern `^## 3\.` matches `## 3.1`, so everything in the
1343
+ forward-only section bled into the LAST rule's file. Anchored on `^## 3. ` now.
1344
+ - **The splitter now WARNS when a rule block cites another rule's verification.** Found in this
1345
+ repository: rule 38's verification paragraph was written after rule 39's block, so the split put
1346
+ it in the wrong file — faithfully, because a splitter is only as correct as the order of the
1347
+ monolith. It cannot repair that automatically. It can report it.
1348
+
1349
+ ### Fixed — the hooks
1350
+
1351
+ - **A path segment was interpolated into an ERE unescaped.** A directory named `app[beta]` became
1352
+ the character class `[beta]` and matched b, e, t or a anywhere, injecting unrelated rules and
1353
+ gotchas. A `(` made grep fail outright and the channel went silent instead.
1354
+ - **The health cache key ignored `AFM_GLOBAL_DIR`.** A session that exported a BROKEN pointer was
1355
+ served the answer from before it was exported, for the rest of the day — silence on the one axis
1356
+ built to break silence.
1357
+ - **A policy value had no cap and no sanitiser.** It is interpolated into the markdown that tells
1358
+ the agent what is not editable, so a value carrying `**` or a newline could close that framing
1359
+ and write its own. Values fold to one line, lose markdown emphasis, and stop at 200 characters.
1360
+ - **The `>= 100` floor was documented and tested, never enforced.** A bundle numbering a rule 37
1361
+ would have collided with the local 37 in every injection, and only the health check would have
1362
+ said so. The floor is applied in the one function every reader goes through.
1363
+ - **The session digest still named `docs/gotchas.md`**, a file this release line deleted.
1364
+
1365
+ ### Fixed — the gates
1366
+
1367
+ - **`refs-lint`'s materialization reading accepted a target that climbs.** `../../PRINCIPLES.md`
1368
+ resolved from the bundle root walks OUT of the plugin, where a coincidentally named file lets a
1369
+ genuinely dead link pass. The exception now applies only to a target with no `..`.
1370
+
1371
+ ### Corrected — a claim
1372
+
1373
+ - The CHANGELOG said three baseline sizes were "held by a test". No test read that file. The sizes
1374
+ are a dated measurement now, with `test/unset-baseline.sh` to re-measure, and the text says what
1375
+ a test actually holds: with `AFM_GLOBAL_DIR` unset the policy layer contributes nothing. They are
1376
+ deliberately not a gate — every one moves when a document changes, and a gate that fires on an
1377
+ ordinary doc edit is a gate somebody disables.
1378
+
1379
+ ### Notes
1380
+
1381
+ - `hooks/test/run.sh`: 155 → 159 assertions. `tools/test/run.sh`: 57 → 60.
1382
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.10.5).** An instance migrated before 3.10.5 may
1383
+ hold frontmatter written by the old splitter. Rule 39 pass 0 names every unparseable file.
1384
+
1385
+ ## [3.10.4] — 2026-08-29
1386
+
1387
+ ### Fixed
1388
+
1389
+ - **The migration produced a file name that was not ASCII.** `tr` and `sed` in a UTF-8 locale read
1390
+ a multi-byte letter as a letter, so a heading with an accent survived the character class that
1391
+ was supposed to remove it. Measured: `002-acentuação-não-ascii-e-emoji.md`. The function
1392
+ documented ASCII and did not deliver it. `LC_ALL=C` makes the byte the unit.
1393
+ - **An empty heading produced `003-.md`**, a name whose only content is a number. It falls back to
1394
+ `untitled` now.
1395
+
1396
+ Both are covered by `tools/test/run.sh`, which grew from 57 to 60 assertions.
1397
+
1398
+ ### Notes
1399
+
1400
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.10.4)** only for an instance that has not
1401
+ migrated yet. A project already split before 3.10.4 may hold a non-ASCII file name from the old
1402
+ slug. `git mv` fixes it, and the reference is the frontmatter, not the name.
1403
+
1404
+ ## [3.10.3] — 2026-08-29
1405
+
1406
+ ### Fixed
1407
+
1408
+ - **A repository-wide scan counted an ephemeral agent worktree.** `.claude/worktrees/` holds full
1409
+ checkouts that an isolated agent fan-out creates, so a walk of the FILESYSTEM sees every file
1410
+ twice. Measured with eight worktrees on disk: the skill listing budget read 59,764 characters
1411
+ against a baseline of 31,524, and the gate failed on a tree nobody ships. `afm-detect.sh` had the
1412
+ same exposure in its repository-wide report. Both now exclude the path.
1413
+
1414
+ ### Notes
1415
+
1416
+ - The general shape, worth carrying: **a gate that walks the filesystem measures whatever happens
1417
+ to be on disk.** A gate that walks git measures the repository. When the two disagree, the
1418
+ filesystem is the one that invents work.
1419
+ - **Consumer-noop.**
1420
+
1421
+ ## [3.10.2] — 2026-08-29
1422
+
1423
+ ### Fixed
1424
+
1425
+ - **`--check-index` regenerated the tree it was supposed to be checking.** `DOCS` was bound once at
1426
+ load, so the `AFM_DOCS_DIR` the check exports for its temporary copy was ignored. The check ran
1427
+ `--materialize` against the REAL documentation, then compared it with itself. It could not fail
1428
+ for the reason it exists, and hard rule 39 depended on it. The directory now resolves at call
1429
+ time.
1430
+ - **`--check-index` could not see a DELETED `index.md`.** It walked only the regenerated side,
1431
+ where a missing file is skipped instead of reported — and a deleted index is the exact failure it
1432
+ exists to catch. It passed at exit 0. The comparison now runs in both directions and reports
1433
+ `missing index`, `stale index` and `orphan index` separately.
1434
+ - **A tab or a newline inside a value corrupted `docs/.afm-index.tsv`.** A description holding a
1435
+ literal tab produced 10 fields where the header declares 9, so every reader that indexes by
1436
+ position read the wrong field with no error. Values are sanitised on write, for the TSV and for
1437
+ the markdown bullet.
1438
+
1439
+ ### Notes
1440
+
1441
+ - All three were found by reviewing this session's own work, and all three share one shape: the
1442
+ check passed for a reason that had nothing to do with what it was checking. A green gate is
1443
+ evidence only when its failure has been observed.
1444
+ - **Consumer-noop.** The changes are inside the tool, which travels with the plugin.
1445
+
1446
+ ## [3.10.1] — 2026-08-29
1447
+
1448
+ ### Fixed
1449
+
1450
+ - **The frontmatter was declared OKF-conformant and no parser had ever read it.** Clause 1 of OKF
1451
+ conformance is "every non-reserved `.md` contains a parseable YAML frontmatter block" (SPEC v0.2
1452
+ §11). **59 files** across the plugin and the instance could not be read by any YAML parser. Every
1453
+ reader in this repository is grep-based, and grep never parses, so the break was invisible.
1454
+
1455
+ Two causes, both introduced earlier in the same release line:
1456
+
1457
+ ```yaml
1458
+ description: Session record: some-name # a colon-space inside a plain scalar
1459
+ trigger_grep: "awk.*\\." # a backslash escape in a double-quoted one
1460
+ ```
1461
+
1462
+ Values that carry a colon or a regex are now **single-quoted**, the YAML style that processes no
1463
+ escape sequence, so a pattern survives it intact.
1464
+
1465
+ - **`afm-index.sh` read only the double-quoted style.** After the fix above it printed the quotes
1466
+ as content, into the generated index and into every line the hook injects. Both of its readers
1467
+ now handle the single-quoted form and undouble the `''` escape.
1468
+
1469
+ - **The `Stop` hook could write an unparseable session file.** The label it derives from the
1470
+ touched files can carry a colon. It now single-quotes `description` and `name`, and the smoke
1471
+ test parses what the hook wrote instead of grepping for field names.
1472
+
1473
+ - **The three catalogue guardrails carried no `type:`.** They declared `id` and `title`, so they
1474
+ were non-conformant from the day OKF was adopted.
1475
+
1476
+ - **The two templates with `{{placeholders}}` now quote them.** That makes the RENDERED file valid,
1477
+ not only the template.
1478
+
1479
+ ### Added
1480
+
1481
+ - **Hard rule 39 gains pass 0.** `python3` parses every frontmatter and requires a non-empty
1482
+ `type` — OKF clauses 1 and 2. It **skips audibly** when `python3` or its `yaml` module is
1483
+ absent, the same discipline the shellcheck skip follows: a silent skip reports coverage the gate
1484
+ does not have. Proven by reintroducing the double-quoted regex and watching the gate fail.
1485
+
1486
+ ### Notes
1487
+
1488
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.10.1).** An instance materialized during 3.9.1
1489
+ through 3.10.0 holds unparseable frontmatter. `/afm:update` must single-quote any value that
1490
+ contains `: `, a `"`, or a backslash. Running rule 39 names every file.
1491
+ - The lesson generalises past YAML. **A format is only adopted once something that did not write
1492
+ the files can read them.** This repository declared conformance to a shared format and kept
1493
+ reading with the tool that hides exactly this class of break.
1494
+
1495
+ ## [3.10.0] — 2026-08-29
1496
+
1497
+ Feature 007. An organisation can now impose documentation on every project on a machine, through
1498
+ one environment variable, and a project that never heard of the feature pays nothing.
1499
+
1500
+ ### Added
1501
+
1502
+ - **`AFM_GLOBAL_DIR` names a second OKF bundle.** Feature 006 is what makes this small: the global
1503
+ directory is an ordinary bundle, with the same `type`, `fires_on` and `trigger_*` keys, so no
1504
+ new format exists and every reader already knows how to read one.
1505
+ - **`hooks/lib/afm-policy.sh`**, sourceable and not a hook. It answers where the bundle is, who it
1506
+ belongs to, whether it may block, and which files a family contributes. Five surfaces need those
1507
+ answers, and five copies of a precedence rule is five places for it to drift.
1508
+ - **`/afm:policy`** — a new op and skill. It reports what the organisation imposes here, read only.
1509
+ It never writes to either bundle.
1510
+ - **A vital sign.** A broken pointer, a bundle with no `organization:`, an organisation rule below
1511
+ the 100 floor, and a number that collides with a local rule. This axis is the ONE place that
1512
+ says the pointer is broken, because every reader stays silent by design.
1513
+
1514
+ ### Changed
1515
+
1516
+ - `afm-session-start.sh` lists the organisation rules after the local ones, named, and says they
1517
+ are not editable in this repository.
1518
+ - `afm-pre-edit.sh` injects a matching organisation rule and gotcha as a separate, named block.
1519
+ - `afm-index.sh --triggers` marks the bundle's rows with an `org:` prefix and the organisation
1520
+ name. `--materialize` never writes into the bundle.
1521
+ - `core/afm.md` documents the optional `policy_blocking` key.
1522
+
1523
+ ### The decisions, and the two that carry weight
1524
+
1525
+ | Question | Decision |
1526
+ | --- | --- |
1527
+ | Precedence | per family. Rules and guardrails go to the organisation. Gotchas, learnings and rubrics accumulate. Product documents never participate |
1528
+ | Rule numbering | an organisation rule is numbered 100 or higher, so the number stays the identity |
1529
+ | Blocking | two consents: `blocking: claim` in the bundle AND `policy_blocking: accept` in `docs/afm.md` |
1530
+ | Read or copy | read live. Hard rule 34 |
1531
+ | Identity | the injected text names the organisation |
1532
+
1533
+ **Why the product family never participates.** It is the defence against a mistyped variable. A
1534
+ path pointing at another PROJECT instead of a policy bundle cannot leak that project's `prd.md`
1535
+ into this session. Requiring `policy.md` is the second half: a directory without one is not read
1536
+ as an organisation.
1537
+
1538
+ **Why blocking needs two consents.** An environment variable is exported by whoever runs the
1539
+ shell. One consent would let that person fail every commit in every repository on the machine.
1540
+
1541
+ ### Deliberately not built
1542
+
1543
+ - **No runner from the policy bundle executes.** A guardrail from the organisation arrives as
1544
+ context, and the gate that decides stays the project's own. Executing shell from a path an
1545
+ environment variable names is a different risk class from reading markdown from it. ADR 0019
1546
+ records it, so the next reader does not treat the absence as an oversight.
1547
+ - **`afm-detect.sh` did not gain a list parser.** The caller already invokes it once per root with
1548
+ a different `AFM_RUBRIC_DIR`. A list would be a second way to do what one variable already does.
1549
+
1550
+ ### Measured
1551
+
1552
+ - **The unset case is byte-identical**, measured on the day: pre-edit injection 3,644 characters.
1553
+ The three sizes are recorded in `test/baseline/007-unset-baseline.tsv` as a dated measurement,
1554
+ and `test/unset-baseline.sh` re-measures them. They are NOT a gate: every one of them moves when
1555
+ a document changes, and a gate that fires on an ordinary doc edit is a gate somebody disables.
1556
+ What a test does hold is the behaviour — with `AFM_GLOBAL_DIR` unset the policy layer
1557
+ contributes nothing at all, asserted in `hooks/test/run.sh`.
1558
+ - The hooks smoke test went from 138 to 154 assertions.
1559
+
1560
+ ### Notes
1561
+
1562
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.10.0), and it is an OPT-IN.** `ops/update.md`
1563
+ gained a section for that shape: a capability delta is reported and never applied, because
1564
+ opting in is the operator's decision. A project that has not exported the variable has not
1565
+ decided against the feature. It has not been asked.
1566
+
1567
+ ## [3.9.3] — 2026-08-29
1568
+
1569
+ ### Known gap — retired
1570
+
1571
+ The eight gaps declared in 3.9.0-rc.7 were re-verified against the tree, one command each, with the
1572
+ default posture that a gap stays open until something proves otherwise. **Six of the eight are
1573
+ closed**, and two more that lived in this repository's instance were checked with them.
1574
+
1575
+ | Gap | State | Proof |
1576
+ | --- | --- | --- |
1577
+ | session-start named a `docs/tools/` path no procedure creates | **closed** | the hook now resolves through a helper that tries `docs/tools/` then `PLUGIN_ROOT`, and emits nothing when neither exists |
1578
+ | the skill count disagreed with what ships | **closed** | 24 directories, and `plugin.json`, `README.md` and `README-light.md` all say 24. `core/protocol.md` names `/afm:protocol` |
1579
+ | `--worktree` never saw an untracked file | **closed** | an untracked probe file appears in `scope_files`, `scope_diff` and `scope_all` |
1580
+ | `ratchet_region` matched a bare prefix | **closed** | `docs/research-extra/x.md` no longer lands in the `docs/research` budget |
1581
+ | the `git commit` detector missed `-c` and `-C` | **closed** | both forms produce `permissionDecision: "deny"` end to end |
1582
+ | `REFLECT_TASKS` was never de-hardcoded | **closed** | `cadence: … tasks=7` reaches both readers through `afm_cadence_read` |
1583
+ | `hooks/README.md` said only `Stop` blocks | **closed** | it now names both carve-outs |
1584
+ | `afm-index.sh --self-test` read the caller's live `./docs/` | **closed** | it passes from `/tmp` at 23 ok |
1585
+ | this repository's `CLAUDE.md` counted 7 hooks | **closed today** | it says 8, and names both blocking hooks |
1586
+ | this repository's `docs/.afm-version` was pinned at 3.5.0-rc.8 | **closed today** | see below |
1587
+
1588
+ ### The pin was lying, and the discovery proved it
1589
+
1590
+ `/afm:update`'s discovery step ran against this repository across the 22 consumer-facing releases
1591
+ since the pin. **Zero deltas are missing.** Ten were already applied, and twelve do not apply here
1592
+ at all, because they copy files into `docs/tools/` and a project that maintains the plugin skips
1593
+ that copy on purpose (hard rule 34).
1594
+
1595
+ The pin now reads 3.9.2, and vital sign A3 stops reporting a drift that was not there. The lesson
1596
+ generalises: an unverified pin reports the version someone last typed, not the version the content
1597
+ is at.
1598
+
1599
+ ### Changed
1600
+
1601
+ - Every feature document carries the contract. The six spec files got a description that says what
1602
+ the feature delivers, not what kind of file it is — the same standard applied to the rest.
1603
+
1604
+ ### Notes
1605
+
1606
+ - **Consumer-noop.** No file a consumer materializes changed in this release.
1607
+
1608
+ ## [3.9.2] — 2026-08-29
1609
+
1610
+ ### Changed
1611
+
1612
+ - **The hard-rule channel caps at two rules, and 1,200 characters each.** Injecting a whole rule
1613
+ instead of a grep line was the right move, and it made that channel the loudest one in the hook:
1614
+ 2,787 characters of the 5,098 an edit of a hook script produced. The rubrics and the gotchas
1615
+ already cap at two for the same reason. A channel that is always loud gets ignored, which looks
1616
+ exactly like coverage while providing none.
1617
+
1618
+ ### Measured
1619
+
1620
+ - One edit of `plugins/afm/hooks/afm-capture.sh`, every family together: **5,098 → 3,644
1621
+ characters.** The two matching gotchas are 1,582 of that, and they arrive complete.
1622
+
1623
+ ### Notes
1624
+
1625
+ - **Consumer-noop.** The change is inside the hook, which travels with the plugin. An instance
1626
+ needs nothing.
1627
+
1628
+ ## [3.9.1] — 2026-08-29
1629
+
1630
+ The OKF profile closes. `ops/update.md` now carries the migration an instance needs, and
1631
+ `interview/{bootstrap,refactor}.md` materialize the new shape from the start.
1632
+
1633
+ ### Added
1634
+
1635
+ - **`ops/update.md` § Structural migrations.** A replacement edits text inside a file. A structural
1636
+ migration changes which files exist, so it cannot be a table of strings, and it must not be prose
1637
+ that every operator re-derives. The plugin ships the migration as an executable and the op runs
1638
+ it: four rows, each with its command, its proof of need and its proof of application.
1639
+ - The section also states what the operator gets for migrating, with the measured numbers, and what
1640
+ happens if they never do: nothing, because every reader carries a legacy branch.
1641
+
1642
+ ### Changed
1643
+
1644
+ - **`interview/bootstrap.md`** materializes `docs/gotchas/` and `docs/rules/` as empty directories
1645
+ instead of a `gotchas.md` seed, and runs `afm-index.sh --materialize` before the final check.
1646
+ Hard rule 39 fails on a missing index, so a bootstrap without that step handed the owner a red
1647
+ gate on their first commit.
1648
+ - **`interview/refactor.md`** follows the same shape.
1649
+ - **ADR 0018** in this repository records the three-layer decision and, more importantly, why the
1650
+ gate stays strict while the instance stays OKF-conformant.
1651
+ - **`CLAUDE.md` documents four plugins.** `ste` has existed since 0.1.0 and was never listed.
1652
+
1653
+ ### Fixed
1654
+
1655
+ - **The rule 30 runner called a release a move backwards.** Its watermark check used `sort -V`,
1656
+ which orders `3.9.1` BEFORE `3.9.1-rc.4` because it reads the hyphen as more text. Semver says
1657
+ the opposite: a pre-release precedes its own release (spec §11.3). Cutting rc.4 to the final
1658
+ 3.9.1 is the last step forward, and the gate blocked it. Found while cutting exactly that
1659
+ release. The comparator now reads the numeric core first, and only then lets the suffix decide.
1660
+
1661
+ ### Notes
1662
+
1663
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.1).** An instance that migrates runs three
1664
+ commands and then commits: `afm-migrate-okf.sh --gotchas`, `afm-migrate-okf.sh --rules`, and
1665
+ `afm-index.sh --materialize`. The `kind:` to `type:` rename is one `sed`. Every step proves it
1666
+ was needed before it runs and proves it applied afterwards.
1667
+
1668
+ ## [3.9.1-rc.4] — 2026-08-29
1669
+
1670
+ ### Changed
1671
+
1672
+ - **`core/rubrics/` moved to `rubrics/`, and `core/templates/` moved to `templates/`.** `core/`
1673
+ now holds only the six canonical documents an instance materializes: `afm`, `ach`, `prd`, `ust`,
1674
+ `gotchas` and `protocol`. It was carrying three different things under one name — the canonical
1675
+ docs, a decision-table catalogue and a set of render templates.
1676
+ - 287 references rewritten (211 relative, 76 absolute). `git mv` first, references second, with
1677
+ no content edit in the same commit: a rename mixed with an edit hides both in the diff.
1678
+
1679
+ ### Fixed
1680
+
1681
+ - **`refs-lint.sh` now reads a `core/` template the way it will be materialized.** A file under
1682
+ `framework-template/core/` becomes a file at the ROOT of the consumer's `docs/`, where
1683
+ `rubrics/x.md` is a sibling. In the plugin it is not, because the template sits one directory
1684
+ deeper. The lint accepts either reading. Without this, the move would have forced every link to
1685
+ be correct in the plugin and broken in every instance.
1686
+
1687
+ ### Notes
1688
+
1689
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.1-rc.4).** Nothing changes in a materialized
1690
+ instance: the moved directories are internal to the plugin, and the materialized names
1691
+ (`docs/rubrics/`, `docs/features/`) were already flat. `/afm:update` needs no step for this.
1692
+
1693
+ ## [3.9.1-rc.3] — 2026-08-29
1694
+
1695
+ ### Added
1696
+
1697
+ - **`afm-index.sh --materialize`** writes an `index.md` in every documentation directory, plus the
1698
+ bundle-root `index.md` that declares `okf_version: "0.2"` — the one place OKF permits it (§12).
1699
+ The entries follow the OKF shape `* [Title](path) - description` and carry no frontmatter (§8).
1700
+ - **`afm-index.sh --check-index`** regenerates into a temporary copy and compares. OKF endorses
1701
+ generating an index and its conformance never checks one against the directory it describes
1702
+ (§11 is structural only), so that check belongs to the producer. Hard rule 39 now runs it.
1703
+ - **`docs/.afm-index.tsv`**, the machine index: one line per concept file with `type`, `load`,
1704
+ `fires_on`, the trigger fields, the path and the description. It is a second artifact because an
1705
+ `index.md` carries no frontmatter, and a regular expression inside a markdown bullet is
1706
+ unreadable for a person and fragile for a parser.
1707
+ - **`/afm:verify <path>`** — a new op and skill, and the ONLY writer of the OKF `verified` field.
1708
+ OKF derives a trust tier from it: absent means unverified, a non-human actor means
1709
+ machine-confirmed, a `human:<id>` actor means human-reviewed (§5.3). AFM is autonomy-first and
1710
+ its own ops write documentation, so until now nothing separated "an agent wrote this and nobody
1711
+ read it" from "a person approved it".
1712
+
1713
+ ### Changed
1714
+
1715
+ - **Hard rule 39 covers the instance families** (`docs/gotchas/`, `docs/rules/`, `docs/adr/`,
1716
+ `docs/rubrics/`) with the same three-key contract, and it exempts the OKF reserved names
1717
+ `index.md` and `log.md`, which are not concept documents (§3.1).
1718
+
1719
+ ### Measured
1720
+
1721
+ - **69 instance files gained the contract**: 15 sessions, 5 evolution reports, 3 protocols, 7
1722
+ research files and 12 rubrics by mechanism, 29 by judgment. The 12 rubrics reuse the canonical
1723
+ frontmatter from the plugin instead of a second hand-written description.
1724
+ - The machine index holds **86 lines** for the whole instance.
1725
+
1726
+ ### Notes
1727
+
1728
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.1-rc.3).** `/afm:update` must run
1729
+ `afm-index.sh --materialize` once after it migrates an instance, or hard rule 39 fails on a
1730
+ missing index. The op file carries that step.
1731
+ - **Why no automatic writer for `verified`.** A hook that stamped it on commit would claim human
1732
+ review of every agent commit, because the agent commits under the operator's git identity. That
1733
+ turns an honest absence into a confident lie, which is worse than having no field.
1734
+
1735
+ ## [3.9.1-rc.2] — 2026-08-29
1736
+
1737
+ ### Changed
1738
+
1739
+ - **A gotcha is a file, and a hard rule is a file.** `docs/gotchas.md` became `docs/gotchas/NNN-slug.md`
1740
+ (26 entries) and section 3 of `docs/afm.md` became `docs/rules/NN-slug.md` (13 rules). Section 3
1741
+ keeps one index line per rule, in the exact `N. **title**` shape that `afm-session-start.sh` and
1742
+ `afm-health.sh` parse, so no hook lost its contract. `afm.md` went from 216 lines to 147.
1743
+ - **`afm-pre-edit.sh` injects the entry, never a pointer.** It used to grep the monolith and then say
1744
+ "read the whole entry", which cost the agent 374 lines to use about 15.
1745
+ - **The gotcha match now uses the SAME shape detector the rubrics use**, pointed at another directory
1746
+ through `AFM_RUBRIC_DIR`. A gotcha fires on `trigger_basename`, `trigger_path` or `trigger_grep`,
1747
+ never on a word that happens to appear in its prose.
1748
+ - **`afm-index.sh` reads both families as files**, with the legacy branch kept for an instance that
1749
+ has not migrated.
1750
+ - **`tools/afm-migrate-okf.sh`** is new. It splits either monolith, counts before and after, and
1751
+ refuses to finish on a difference. It writes no trigger it cannot derive from the text.
1752
+
1753
+ ### Fixed
1754
+
1755
+ - **`afm-detect.sh` never fired on a file that does not exist yet.** A `Write` of a new file is
1756
+ exactly when the advice is worth most, because the mistake has not happened. The name and path
1757
+ triggers now apply, and only the content conditions skip. Self-test covers it.
1758
+
1759
+ ### Measured
1760
+
1761
+ - The trigger index: **8,628 characters and 46 entries → 6,602 characters and 58 entries.** About 500
1762
+ tokens cheaper while covering 12 more decision surfaces.
1763
+ - **9 of the 26 gotchas have a mechanical trigger. The other 17 declare `fires_on: event`** and say
1764
+ why. That is not a gap. It is the honest count: two thirds of the gotchas describe a moment a human
1765
+ recognises, and the old text match fired them on coincidence.
1766
+ - **`load:` saves nothing yet, and the measurement file says so.** The index never scanned a cold
1767
+ family, so the skip has no work to do until the generated index lands.
1768
+
1769
+ ### Notes
1770
+
1771
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.1-rc.2).** An instance keeps its monolith until
1772
+ `/afm:update` runs. Every reader — the pre-edit hook, the index, the health check — carries a
1773
+ legacy branch, so an unmigrated instance loses nothing. Without that branch, updating the PLUGIN
1774
+ would silently switch off rule injection in every instance that had not migrated, and nothing
1775
+ would report it.
1776
+
1777
+ ## [3.9.1-rc.1] — 2026-08-29
1778
+
1779
+ ### Changed
1780
+
1781
+ - **The frontmatter contract becomes OKF v0.2.** `kind:` is now `type:`, the one key the
1782
+ Open Knowledge Format requires. 82 files renamed. The AFM extension keys (`fires_on`,
1783
+ `trigger_*`, `writes`, `composes`) are untouched, because OKF protects unknown keys and
1784
+ forbids a consumer from rejecting a document that carries them.
1785
+ - **Two keys join it on every indexed file.** `description:` is one sentence, and the index
1786
+ prints it instead of opening the body. `load:` is `hot`, `warm` or `cold`, and it tells the
1787
+ index what it may skip. 62 files carry all three.
1788
+ - **Hard rule 39** now demands the three keys, and rejects the retired `kind:`.
1789
+ - **Hard rule 36** now forbids a FILESYSTEM absolute path. A leading `/` means the bundle
1790
+ root, which is the link form OKF recommends because it survives a file move.
1791
+ - **`afm-index.sh` reads `type`.** The `--kind` flag is now `--type`. No compatibility shim:
1792
+ the rename is mechanical and the gate proves it landed.
1793
+ - **The `Stop` hook writes the contract** on a session file: `type`, `description` and
1794
+ `load: cold`.
1795
+
1796
+ ### Fixed
1797
+
1798
+ - **`refs-lint.sh` used to skip every `/`-prefixed link.** That made the safest link form the
1799
+ only unverified one. It now resolves a bundle-relative link from the bundle root, with a
1800
+ self-test in both directions.
1801
+
1802
+ ### Notes
1803
+
1804
+ - **`tier:` was already taken.** The `Stop` hook has written `tier: dual|episodic` on session
1805
+ files since 3.4.0-rc.7, meaning the CoALA memory tier. The new key is `load:`, which names
1806
+ what it controls. No history was rewritten.
1807
+ - **Rule 39 used to forbid a `summary:` key**, because a derived duplicate goes stale in
1808
+ silence. `description:` reverses that decision. The reversal is written into the rule with
1809
+ the trade named. The objection is sound, and it is paid for one reason: without a stated
1810
+ description the index cannot say what a file is without reading it.
1811
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.1-rc.1).** An instance materialized before
1812
+ 3.9.1 carries `kind:` in its frontmatter, and no `description:` or `load:`. The index in that
1813
+ instance reads `type:` after `/afm:update`, so the rename must land in the instance too.
1814
+ `/afm:update` renames the key, and reports every file that still needs a `description:` — it
1815
+ never invents one, because a wrong description is worse than an absent one (principle #5).
1816
+ - Version **3.9.1**, not 4.0.0. The convention calls a layout change a major bump. The owner
1817
+ reserved 4.0.0 for a larger change, and decided this exception on 2026-08-29.
1818
+
1819
+ ## [3.9.0-rc.12] — 2026-08-28
1820
+
1821
+ ### Fixed
1822
+
1823
+ > **A second validation pass rebuilt each rc.11 fix and hunted its neighbours.** Five of six were
1824
+ > closed only for the input that had been named. The sixth — the commit detector — turned out not
1825
+ > to be closeable by enumeration at all, and is now bounded and declared instead of quietly widened
1826
+ > one more time.
1827
+
1828
+ - **The commit detector anchored on the wrong concept: the first token, not COMMAND POSITION.**
1829
+ Eight everyday spellings ran a real commit with the gate red and the hook silent, each verified
1830
+ by executing it: `(git commit)`, `{ git commit; }`, `command git commit`, `env git commit`,
1831
+ `/usr/bin/git commit`, `"git" commit`, `sh -c "git commit"`, and `git --attr-source TREE commit`.
1832
+ The anchor now accepts the shell's own command-opening constructs, a path prefix, a quote, and a
1833
+ **closed list** of exec wrappers and shells — closed because no regex can tell `command git`
1834
+ (executes) from `echo git` (does not). `--attr-source` joins the value-taking globals.
1835
+
1836
+ - **The limitation is now declared where a reader looks.** `hooks/README.md` states plainly that
1837
+ tier 3 is a **nudge for the common path, not a boundary**: it cannot see a command built at
1838
+ runtime (`eval`, a variable, a script on disk), because a regex over a command string cannot
1839
+ enumerate a shell. The boundary is `--install`, which writes git's own `pre-commit` hook — git
1840
+ runs that however the command is spelled. Every release that widens the regex buys one more
1841
+ spelling, not closure, and saying so is worth more than the next widening.
1842
+
1843
+ - **The hooks harness fabricated the failure it was looking for.** `pc()` interpolated the command
1844
+ into a JSON string, so any probe containing a double quote built INVALID json; the hook's `jq`
1845
+ returned empty, the hook exited 0, and the assert read that silence as "the detector missed it".
1846
+ Two cases were accused of slipping past a hook that was in fact denying them. It now encodes
1847
+ through `jq -n --arg`.
1848
+
1849
+ - **A repeated cadence key resolved to the LAST occurrence.** The `sed` had a greedy leading `.*`,
1850
+ so an operator who wrote `tasks=7 (raised from tasks=2)` silently got 2 — the value they had just
1851
+ moved away from. `afm_cadence_key` now tokenizes the line and takes the first match, which also
1852
+ gives the word boundary for free instead of encoding it in a pattern.
1853
+
1854
+ - **A tool whose plugin root was unresolvable answered EMPTY with exit 0.** The symlink walker
1855
+ added in rc.11 covers a symlink; a hard link carries no back-reference, and neither does a copy
1856
+ of the script dropped elsewhere. The check moved to the RESULT instead of the invocation shape:
1857
+ no `ops/` under the resolved root means `afm-index.sh` prints what is wrong on stderr and
1858
+ abstains with **exit 2**. An empty answer with exit 0 reads as "the plugin declares nothing".
1859
+
1860
+ - **A ratchet budget bash cannot compare.** The TSV is hand-written, and an all-digit value beyond
1861
+ the 64-bit range survives `awk` and then breaks `[ -le ]` itself with *integer expression
1862
+ expected*, which evaluates falsy — the finding counted as over budget and stderr filled up on
1863
+ every run. `ratchet_budget` clamps anything above 9 digits to 0, the same answer an unregistered
1864
+ rule gets: a budget nobody can read is a budget nobody measured.
1865
+
1866
+ - **`gate_skill_count` had two blind spots.** A declared count of **0** was conflated with "no
1867
+ count declared", which silently exempted the whole plugin from the second pass; `max` and
1868
+ `found` are now separate. And the singular — "three atomic skill", "skill count: 23" — evaded
1869
+ both passes, which looked only for the plural.
1870
+
1871
+ - **`hooks/README.md` claimed `afm-session-end.sh` had a `SessionEnd` fallback.** `hooks.json` has
1872
+ never carried that key and the script never read one.
1873
+
1874
+ - **Two harness blocks vanished with no output when `git` was absent.** About 25 asserts in
1875
+ `hooks/test/run.sh` — the session-end handoff block and the whole `Stop` cadence block, which
1876
+ covers the only hook authorized to block — sat inside `if command -v git` with no else branch.
1877
+ A block that disappears silently is indistinguishable from a block that passed. The harness
1878
+ gained a `skip()` that always speaks and a skip count in its summary line, matching the sibling
1879
+ harness. Proven by running it with `git` off `PATH`.
1880
+
1881
+ - **The sibling harness had the same hole, in the place that already knew about it.** The comment
1882
+ above the repo-shim walk-up in `tools/test/run.sh` records that those two asserts "skipped in
1883
+ silence for the whole run" — the walk-up was fixed then and the silence was not, so the pass
1884
+ count still moved between 55 and 57 depending on where the file sat. **A harness whose pass
1885
+ count moves with its location is reporting its location, not its coverage.**
1886
+
1887
+ - **One more JSON-interpolation site.** `CAP` built a capture payload by interpolating a command
1888
+ into a JSON string, the exact trap that `pc()` had just been fixed for. It had not bitten only
1889
+ because no probe there carries a quote, and "not yet" is not a property.
1890
+
1891
+ ### Consumer migration
1892
+
1893
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.0-rc.12).** An instance that materialized
1894
+ `docs/tools/gate/` holds a stale `lib/ratchet.sh`: without the clamp, a hand-typed budget can
1895
+ make every finding in that region count as over budget while stderr fills with *integer
1896
+ expression expected*. `/afm:update` re-copies `gate/lib/ratchet.sh`. The instance's
1897
+ `gate/rules/` and `gate/ratchet.tsv` are never touched.
1898
+
1899
+ ## [3.9.0-rc.11] — 2026-08-28
1900
+
1901
+ ### Fixed
1902
+
1903
+ > **Every residue the validation left behind is closed.** rc.10 fixed the two routes the probe
1904
+ > ranked highest and *declared* the rest. Declaring is cheaper than fixing only while the list
1905
+ > is short — this empties it.
1906
+
1907
+ - **`git --config-env NAME=ENVVAR commit` reached a commit and the detector stayed silent.**
1908
+ Measured: `MYVAL=false git --config-env core.autocrlf=MYVAL commit -m x` creates a real
1909
+ commit. It joins the enumerated value-taking globals. Two siblings a probe reported alongside
1910
+ it are deliberately **not** enumerated: `--super-prefix` exits 129 as an unknown option, and
1911
+ `--html-path` prints a path and exits before `commit` is ever parsed. Neither can commit, and
1912
+ a form that cannot commit is not a bypass — adding it would only widen the pattern.
1913
+
1914
+ - **A cadence key was matched as a substring, not as a word.** `*tasks=*` also matched
1915
+ `subtasks=9`, so a neighbouring key silently donated its value to the threshold; the same
1916
+ weakness had been there for `reflect=` and `evolve=` since v3.5.0, and `tasks=` copied it
1917
+ verbatim in rc.9. All three keys now go through one `afm_cadence_key` helper that matches at a
1918
+ word boundary. **A parser that answers confidently from a key nobody wrote is worse than one
1919
+ that answers the default.**
1920
+
1921
+ - **A value bash cannot compare was treated as a threshold.** An all-digit number beyond the
1922
+ 64-bit range passed the "is it numeric" sanitizer and then broke `[ -lt ]` itself with
1923
+ *integer expression expected*, which evaluates falsy — so the `Stop` hook fell **through to
1924
+ block** on a threshold the operator had declared as astronomically high, printing stderr noise
1925
+ on every session end. Values above 9 digits now fall back to the documented default.
1926
+
1927
+ - **A tool invoked through a symlink could not find its own plugin tree.** `dirname "$0"`
1928
+ answers the symlink's directory, so `ROOT` pointed somewhere with no `ops/` and the
1929
+ plugin-controlled queries came back **empty** — which reads exactly like "the plugin declares
1930
+ no rubrics" and would be believed. `afm-index.sh` and `afm-detect.sh` now walk the symlink
1931
+ chain (a six-line loop, not `readlink -f`, which is GNU-only). The resolver is duplicated in
1932
+ the two scripts on purpose: a shared library would itself have to be located, and locating it
1933
+ is the problem this solves. `tools/test/run.sh` now runs every self-testing tool through a
1934
+ symlink as well as directly.
1935
+
1936
+ - **`gate_skill_count` went quiet when a count was reworded out of shape.** It reads the literal
1937
+ `N (atomic )?skills`; spell the number out and the file stopped matching, and "no match" meant
1938
+ "nothing to check" — the file passed while carrying a stale claim. A second pass now names any
1939
+ file that mentions skills and declares no countable number, **but only in a plugin where a
1940
+ sibling file does declare one**: `learn`, `seo` and `ste` state no count anywhere, and
1941
+ demanding one from them would be a different rule than this.
1942
+
1943
+ - **The `Wiring` bullet in `hooks/README.md` listed one `PreToolUse` entry; `hooks.json` has
1944
+ two.** The missing one is `afm-pre-commit.sh`, the entry that can refuse. A reader scanning
1945
+ the inventory bullet would have come away with the same wrong picture rc.8 corrected in the
1946
+ two other bullets.
1947
+
1948
+ ### Consumer migration
1949
+
1950
+ - **Consumer-noop.** `afm-index.sh` and `afm-detect.sh` are not materialized by any consumer
1951
+ procedure — the session-start hook resolves them from the plugin since rc.8. `CONSUMER_VERSION`
1952
+ stays at 3.9.0-rc.9.
1953
+
1954
+ ## [3.9.0-rc.10] — 2026-08-28
1955
+
1956
+ ### Fixed
1957
+
1958
+ > **Found by validating rc.8 and rc.9, not by reading them.** Each of the eight closed gaps was
1959
+ > rebuilt as a scenario, run against the pre-fix commit to prove it went red, then run against
1960
+ > the fix. Seven held. The eighth held only for the defect as declared — the same hook was
1961
+ > reachable by two neighbouring routes nobody had named.
1962
+
1963
+ - **The `git commit` detector anchored on the wrong thing.** rc.9 widened the option group and
1964
+ left the ANCHOR untouched: `(^|[;&|][[:space:]]*)git` demands `git` at position 0 or right
1965
+ after a separator, so two everyday shell forms stayed invisible — an inline environment
1966
+ assignment (`GIT_AUTHOR_NAME=x git commit`, which is how authorship gets set on one line) and
1967
+ leading whitespace (which comes free with any indented heredoc). Both execute normally, and
1968
+ both left the tier-3 gate silent. The anchor now allows leading whitespace and a run of
1969
+ `NAME=value` assignments. An assignment is never a bare word, so `foo git commit` — git as an
1970
+ argument to something else — still does not match. Verified in both directions: seven commit
1971
+ forms denied, six read-only forms untouched.
1972
+
1973
+ - **`gate_skill_count` had no `--self-test` coverage.** The gate added in rc.8 was proven only
1974
+ by the real run, which can say nothing stronger than "the numbers agree today". This
1975
+ repository already paid for that once: an `is_known_debt` assert named a real baseline row,
1976
+ the row left the baseline, and the assert stayed red for months because no gate ran
1977
+ `--self-test`. Four synthetic-fixture asserts now prove the gate catches a 24th skill, names
1978
+ every stale file, **stays red while any single file is stale** (the per-file property that
1979
+ stops a corrected README masking a stale `plugin.json`), and tolerates a smaller historical
1980
+ count in the prose. The gate reports through the harness counters, so each probe runs inside
1981
+ a command substitution and only the delta comes back.
1982
+
1983
+ ### Known gap
1984
+
1985
+ - **Two low-severity residues, declared rather than fixed.** `afm_cadence_read` matches its keys
1986
+ with unanchored globs, so a future key ending in `tasks=` would be mis-read — no such key
1987
+ exists today. And `afm-index.sh` resolves its own directory without `readlink -f`, so invoking
1988
+ it through a symlink points `ROOT` at the symlink's directory; nothing in the shipped wiring
1989
+ invokes it that way. Both are written down here so the next probe does not re-find them.
1990
+
1991
+ ### Consumer migration
1992
+
1993
+ - **Consumer-noop.** No file a consumer materializes changed. `CONSUMER_VERSION` stays at
1994
+ 3.9.0-rc.9.
1995
+
1996
+ ## [3.9.0-rc.9] — 2026-08-28
1997
+
1998
+ ### Fixed
1999
+
2000
+ > **The last four gaps declared in `3.9.0-rc.7` are closed.** With rc.8 this closes all eight.
2001
+ > Every fix landed with the failing assert first.
2002
+
2003
+ - **`afm-pre-commit.sh` did not recognize a `git commit` carrying a global option that takes a
2004
+ value.** The option group accepted dash-tokens only, so in `git -c core.hooksPath= commit` the
2005
+ value token matched nothing, the whole pattern failed, and the hook exited 0 — **the tier-3
2006
+ gate no-opped instead of denying, and from the outside a no-op is indistinguishable from a
2007
+ green gate**. `git -C <dir> commit` is the same shape, and it is how an agent commits inside
2008
+ another worktree. The list of value-taking globals is **enumerated** (`-c`, `-C`,
2009
+ `--git-dir`, `--work-tree`, `--namespace`, `--exec-path`), not a general "any token":
2010
+ widening the group to any token would have made `git log --grep commit` a denial. Five
2011
+ asserts cover both directions.
2012
+
2013
+ - **A ratchet region matched as a character prefix, with no path boundary.** `index(p,$2)==1`
2014
+ meant `docs/research-extra/x.md` started with the characters of `docs/research` and spent the
2015
+ neighbouring region's budget. A budget that absorbs a directory nobody measured stops meaning
2016
+ anything, and it fails toward green. The prefix must now end at a `/` or at the end of the
2017
+ path; the match stays literal (no regex — a path holding `.` or `+` would become a
2018
+ metacharacter). A trailing slash written by hand is tolerated.
2019
+
2020
+ - **`REFLECT_TASKS` was never actually de-hardcoded.** `hooks/README.md` said the thresholds had
2021
+ stopped being hardcoded in v3.5.0 and were declarable through the `cadence:` line, and
2022
+ `core/protocol.md § 2` declared the parameter with a default of 4 — but `afm_cadence_read`
2023
+ parsed only `reflect=` and `evolve=`. The value therefore survived as a bare literal in
2024
+ **both** readers: `REFLECT_TASKS=4` in `afm-session-end.sh` (the hook that blocks) and a bare
2025
+ `-ge 4` in `afm-health.sh`. The library exists so two readers cannot drift, and on this one
2026
+ parameter they had already drifted in form. `afm_cadence_read` now returns three fields and
2027
+ the `cadence:` line takes `tasks=`, with `tasks=off` disabling the per-task arm alone —
2028
+ symmetric with the other two keys, because a key that takes a number but not `off` while its
2029
+ neighbours take both is the inconsistency that becomes a gotcha later.
2030
+
2031
+ - **`afm-index.sh --self-test` asserted against the caller's live `./docs/`.** The consumer half
2032
+ of `--triggers` reads `./docs/procedures`, `./docs/learnings`, `./docs/gotchas.md` and
2033
+ `./docs/afm.md` relative to the caller, and the self-test ran it from wherever the operator
2034
+ stood — with a hardcoded count of 3 procedures. It was green only from the root of the
2035
+ repository that maintains the plugin, never green in a consumer, and `tools/test/run.sh` was
2036
+ red when invoked from its own documented directory. **A self-test that mirrors the caller
2037
+ tests nothing; it reports where you were standing.** It now builds its own consumer fixture
2038
+ and runs that half inside it, while `ROOT` stays on the real plugin for the asserts that are
2039
+ genuinely about what ships. Verified green from three different working directories,
2040
+ including the one that was red.
2041
+
2042
+ ### Changed
2043
+
2044
+ - **`hooks/README.md` and `core/protocol.md § 2` now show the cadence line in full** —
2045
+ `cadence: reflect=1 evolve=3 tasks=4` — and state that every key accepts `off`.
2046
+
2047
+ ### Consumer migration
2048
+
2049
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.0-rc.9).** An instance that materialized
2050
+ `docs/tools/gate/` holds a stale `lib/ratchet.sh`: without the boundary fix, a region in its
2051
+ ratchet silently covers every sibling directory that shares the same prefix. `/afm:update`
2052
+ re-copies `gate/lib/ratchet.sh`. The instance's `gate/rules/` and `gate/ratchet.tsv` are
2053
+ **never** touched. Expect the gate to start reporting findings that were being charged to a
2054
+ neighbouring budget; that is the fix working, not a regression.
2055
+
2056
+ - **New optional key in `docs/_focus.md`:** `cadence:` accepts `tasks=N` (or `tasks=off`)
2057
+ alongside `reflect=` and `evolve=`. Nothing changes for an instance that does not write it —
2058
+ the default stays 4, exactly the literal that used to be hardcoded.
2059
+
2060
+ ## [3.9.0-rc.8] — 2026-08-28
2061
+
2062
+ ### Fixed
2063
+
2064
+ > **Four of the eight gaps declared in `3.9.0-rc.7` are closed.** Each one landed with the
2065
+ > failing assert first: the harness reproduced the gap, then the fix turned it green. The four
2066
+ > that remain (the `git -c`/`git -C` blind spot in `afm-pre-commit.sh`, the ratchet prefix that
2067
+ > bleeds into a neighbouring region, `REFLECT_TASKS` still hardcoded, and `afm-index.sh
2068
+ > --self-test` asserting against the caller's live `docs/`) stay declared in rc.7, unfixed.
2069
+
2070
+ - **`afm-session-start.sh` no longer names a path that does not exist.** It gained the
2071
+ `PLUGIN_ROOT` its sibling `afm-pre-edit.sh` already had, and now resolves the frontmatter
2072
+ index in three steps: the materialized `docs/tools/afm-index.sh` when the consumer has it,
2073
+ the plugin copy otherwise, and — when neither resolves — **it omits the bullet entirely**.
2074
+ The old text hardcoded the consumer path unconditionally, so following the injected
2075
+ instruction exited 127 both in a fresh consumer and in a project that maintains the plugin,
2076
+ where `bootstrap.md` Step 7 says the copy is skipped on purpose. An instruction that exits
2077
+ 127 costs more than a missing instruction; the hook now abstains instead of teaching a
2078
+ broken command. Four asserts in `hooks/test/run.sh` cover the three branches.
2079
+
2080
+ - **`hooks/README.md` said only `Stop` can refuse. `afm-pre-commit.sh` refuses too.** The file
2081
+ documented the second carve-out in its own body since 3.5.0-rc.14 and still opened with "one
2082
+ named, enumerated exception", including in the `Output` bullet — the line somebody reads to
2083
+ learn what can stop them. Both lines now name the two: `decision:"block"` on `Stop` and
2084
+ `permissionDecision:"deny"` on `PreToolUse`.
2085
+
2086
+ - **The plugin ships 23 skills and said 22.** Corrected in `README.md` (twice),
2087
+ `README-light.md` and `plugin.json`. `protocol` now appears in the skill tree, in the
2088
+ overview list of `README.md`, in `README-light.md`, and `core/protocol.md` finally names
2089
+ `/afm:protocol` — until now the only file `PRINCIPLES.md` links for the protocol layer
2090
+ never told the reader which skill drives it. **The count is now mechanical:**
2091
+ `test/skills-lint.sh` gained `gate_skill_count`, which compares the number a human typed
2092
+ against the number `ls` answers. It checks per file and on the largest count in that file,
2093
+ because `README.md` legitimately cites historical counts ("before v2.0.0 the plugin had only
2094
+ 2 skills"). Per file and not per plugin on purpose: a plugin-wide maximum would have let the
2095
+ corrected `README.md` hide the `plugin.json` that was still wrong — which is the exact shape
2096
+ this bug had.
2097
+
2098
+ - **The gate saw the working tree without the files that had just been created.**
2099
+ `scope_files worktree` asked `git ls-files` and `scope_diff worktree` asked `git diff HEAD`,
2100
+ and an untracked file is in neither. A brand new secret (rule 13) or a brand new document in
2101
+ the wrong language (rule 37) passed at **exit 0** — and exit 0 from a gate reads as "I looked
2102
+ and it is clean", never as "I did not look". `worktree` now adds `git ls-files --others
2103
+ --exclude-standard`, and `scope_diff` synthesizes the missing diff with `git diff --no-index
2104
+ /dev/null <file>`, which is read-only (`git add -N` would give the same output by writing to
2105
+ the index, and a gate does not write). `--staged` deliberately still ignores them: a file
2106
+ nobody added is not entering this commit, and failing on it would be the false positive that
2107
+ makes people work around the gate. Measured on this repository after the fix: an untracked
2108
+ doc carrying an absolute path takes `--worktree` to exit 1 and leaves `--staged` at 0.
2109
+
2110
+ ### Changed
2111
+
2112
+ - **`plugin.json`'s `description` is now English** (hard rule 37). It was the largest piece of
2113
+ Portuguese prose left on a consumer-facing surface — it is what the marketplace displays.
2114
+
2115
+ - **`scope_all` reads `AFM_GATE_MODE`**, exported by `guardrails.sh`. Its signature stays
2116
+ `scope_all [pathspec...]`: every runner in this repository, and every runner a consumer has
2117
+ already written, calls it that way, so growing a mode parameter would have been a breaking
2118
+ change for consumers.
2119
+
2120
+ ### Consumer migration
2121
+
2122
+ - **Consumer-facing (bumps `CONSUMER_VERSION` → 3.9.0-rc.8).** An instance that materialized
2123
+ `docs/tools/gate/` holds a **stale copy of `lib/scope.sh`**, which is the file that decides
2124
+ what `--worktree` can see. `/afm:update` re-copies `gate/lib/scope.sh` and
2125
+ `gate/guardrails.sh` over the instance's copies. The instance's own `gate/rules/` and
2126
+ `gate/ratchet.tsv` are **never** touched: a runner encodes that project's rule and the
2127
+ ratchet holds measured debt. Expect `--worktree` to find debt it could not see before; that
2128
+ is the point of the fix, and it is not a reason to rebaseline the ratchet without looking.
2129
+
2130
+ - The hook and documentation fixes need nothing from the consumer: hooks travel with the
2131
+ plugin, and no materialized file changed because of them.
2132
+
2133
+ ## [3.9.0-rc.7] — 2026-08-27
2134
+
2135
+ ### Known gap
2136
+
2137
+ > **This release changes no behaviour.** It is a declaration. An external probe of the plugin
2138
+ > at three intensities produced 12 findings; each one was then handed to an independent
2139
+ > validator whose default posture was REFUTED and whose job was to reproduce it against the
2140
+ > real tree. **Two were refuted** and are recorded below so they are not re-found. The eight
2141
+ > that survived are declared here, unfixed, because a gap that is written down is cheaper to
2142
+ > reason about than a gap that is fixed in the same breath it is discovered.
2143
+
2144
+ **Reachability — the two that reach a consumer first.**
2145
+
2146
+ - **`afm-session-start.sh` names `docs/tools/afm-index.sh` and `afm-detect.sh` in the context it
2147
+ injects into every session, and no procedure ever puts those two files there.**
2148
+ `interview/bootstrap.md` and `interview/refactor.md` enumerate a closed list for
2149
+ `<consumer>/docs/tools/` — `afm-cost-ledger.sh`, `afm-outcome-join.sh`, `README.md`, `gate/` —
2150
+ and `afm-index.sh`, `afm-detect.sh`, `afm-lang.sh` appear in no consumer-facing procedure at
2151
+ all. Following the injected instruction exits **127**, in a fresh consumer and in this
2152
+ repository today. The sibling hook already does it right: `afm-pre-edit.sh` resolves the same
2153
+ scripts through `${CLAUDE_PLUGIN_ROOT}/framework-template/tools/`. The session-start hook has
2154
+ no `PLUGIN_ROOT` at all — it hardcodes a consumer-relative path into prose.
2155
+
2156
+ Note the shape: **the index the whole of 3.9.0 was built to make cheap is the thing the
2157
+ consumer cannot run.** `bootstrap.md` states that a project maintaining the plugin *skips*
2158
+ the copy on purpose, so pointing at a consumer-relative path unconditionally is wrong on both
2159
+ sides of that fork, not merely stale.
2160
+
2161
+ - **`README.md`, `README-light.md` and `plugin.json` all say 22 atomic skills; 23 ship.**
2162
+ `protocol` is the one missing from every newcomer-facing enumeration, and `core/protocol.md`
2163
+ — the only file `PRINCIPLES.md` links for the protocol layer — never names `/afm:protocol`.
2164
+ A skill absent from the surface a newcomer reads is a skill that does not exist for them.
2165
+ `CHANGELOG.md` already counts 23, so the disagreement is internal.
2166
+
2167
+ **Mechanism — a gate that answers when it should abstain, or abstains where it should answer.**
2168
+
2169
+ - **`--worktree` never sees an untracked file.** `scope_diff`, `scope_files` and `scope_all` in
2170
+ `tools/gate/lib/scope.sh` build their scope from `git diff HEAD` and `git ls-files`. A file
2171
+ that was just created and never staged is in neither. A brand-new secret (rule 13) or a
2172
+ brand-new Portuguese doc (rule 37) passes the worktree gate at **exit 0** — and exit 0 from a
2173
+ gate is read as "checked and clean", not as "did not look".
2174
+
2175
+ - **`ratchet_region` matches a region as a bare character prefix.** `index(p, $2) == 1` in
2176
+ `tools/gate/lib/ratchet.sh`, with no path-boundary test, so `docs/research-extra/…` is
2177
+ absorbed into the budget configured for `docs/research`. Debt lands in a neighbour's budget
2178
+ and the gate stays green. Same family as the count that always answers zero: it fails in the
2179
+ direction that looks like it works.
2180
+
2181
+ - **The `git commit` detector in `afm-pre-commit.sh` misses `git -c K=V commit` and
2182
+ `git -C DIR commit`.** The pre-subcommand group tolerates only single-token global options,
2183
+ so any global option carrying a separate-token value slips the match and the tier-3 gate
2184
+ **no-ops instead of denying**. The fix has to enumerate the separate-value options rather
2185
+ than accept a wildcard token, or it will deny on a pathspec.
2186
+
2187
+ **Doc-versus-code.**
2188
+
2189
+ - **The `REFLECT_TASKS` threshold was never actually de-hardcoded.** `hooks/README.md` states
2190
+ the thresholds "stopped being hardcoded in v3.5.0" and are "declarable per project via the
2191
+ `cadence:` line", but `afm_cadence_read()` in `lib/afm-cadence.sh` parses only `reflect=` and
2192
+ `evolve=` and echoes two fields. The task threshold is the literal `REFLECT_TASKS=4` in
2193
+ `afm-session-end.sh` and a bare `-ge 4` in `afm-health.sh`. The single-source library exists
2194
+ precisely to stop two readers from drifting, and the two readers have already drifted in
2195
+ form — one names a variable, the other does not. The hook that **blocks** holds one of the
2196
+ two copies.
2197
+
2198
+ - **`hooks/README.md` still says no hook returns `decision:"block"` except `Stop`.**
2199
+ `afm-pre-commit.sh` returns `permissionDecision: "deny"`. The file names that script once and
2200
+ contradicts it in the bullet a reader consults to learn what can stop them.
2201
+
2202
+ - **`afm-index.sh --self-test` asserts against the caller's live `./docs/`, not a fixture.**
2203
+ It passes only when the cwd is this repository's root, and its hardcoded count of three
2204
+ procedures can never be green in a consumer project — so the documented smoke test
2205
+ (`tools/README.md` § Self-test, `./test/run.sh`) is red from its own documented directory. A
2206
+ self-test that reads the tree it is meant to be independent of measures the caller, not the
2207
+ code.
2208
+
2209
+ **Refuted — recorded so they are not re-found.**
2210
+
2211
+ - *"The Portuguese `**Gatilho:**` spelling is invisible to `afm-pre-edit.sh`."* The behaviour
2212
+ reproduces, and it is already declared verbatim in this changelog's own consumer-migration
2213
+ note for 3.6.0, and again as a known gap in 3.9.0-rc.6. Declared is not defective.
2214
+ - *"`--writes` reports a path that does not exist."* A misreading. An op's `writes:` is a
2215
+ consumer-relative **output contract**, not an assertion about a path in the plugin tree.
2216
+
2217
+ **Two more sit in this repository's instance, not in the plugin**, and are recorded in
2218
+ `docs/_focus.md` rather than here: `CLAUDE.md` counts 7 hook scripts where 8 ship and calls
2219
+ `Stop` the only blocking hook, and `docs/.afm-version` is pinned at `3.5.0-rc.8` against a
2220
+ `3.9.0-rc.6` watermark, so vital sign A3 reports drift on every session.
2221
+
2222
+ - **Consumer-noop.** No file a consumer materializes changed. `CONSUMER_VERSION` stays at
2223
+ `3.9.0-rc.6`.
2224
+
2225
+ ## [3.9.0-rc.6] — 2026-08-27
2226
+
2227
+ ### Added
2228
+
2229
+ - **Every family with a trigger reaches the index — 58 entries across six of them.** Rubric
2230
+ (12), gotcha (26), hard rule (13), guardrail (3), procedure (3), learning (1). The whole
2231
+ index is **~2,157 tokens** against the **~35,465 tokens** of the bodies it stands in for.
2232
+
2233
+ - **`fires_on` gained `event` and `cadence`**, and those two values are the honest part of this
2234
+ release. They mark a trigger that **cannot fire on its own** — a moment a human recognizes.
2235
+ A `procedure` that starts "when a release is about to be cut" will never be fired by a hook,
2236
+ and it still belongs in the index, because the question it answers is not "what fires now"
2237
+ but "is there a runbook for this".
2238
+
2239
+ An `event` or `cadence` entry carries **no `trigger_*` key**, and hard rule 39 now fails one
2240
+ that does. Fabricating a mechanical field for a human judgment would dilute exactly what the
2241
+ mechanical families earned by being genuinely mechanical.
2242
+
2243
+ ### Fixed
2244
+
2245
+ - **The survey in 3.9.0-rc.5 answered the wrong question, and the owner caught it.** Four
2246
+ families were assessed and all four said no. Every one of those verdicts was about
2247
+ **firing at write time**, which needs a mechanical file-shape trigger — and on that question
2248
+ they were right. None of them was about **discovery**, which needs nothing mechanical at all:
2249
+ only the condition written where a reader can see it without opening the body.
2250
+
2251
+ Two separable benefits behind one word. Procedures are the family where the second one pays
2252
+ most: three runbooks whose start condition lived in a `When to run it` bullet inside the
2253
+ body, so answering "is there a runbook for this?" meant opening all three. This repository
2254
+ reached 2026-08 with **26 gotchas and 3 procedures**, and the family nobody creates is the
2255
+ family nobody can find.
2256
+
2257
+ ### Changed
2258
+
2259
+ - **Gotchas and hard rules are indexed WITHOUT frontmatter**, and that is deliberate. Each is a
2260
+ section of one file, never a file of its own, so there is no frontmatter block to carry and
2261
+ there never will be. They are already structured — a `**Trigger:**` line under a heading, a
2262
+ numbered rule with a `*Verification:*` — so the index reads that structure directly. The
2263
+ point is one cheap line per decision surface, not one YAML block per artifact.
2264
+
2265
+ - The gotcha reader accepts **both spellings** of the field. A consumer bootstrapped before the
2266
+ english-first migration still writes the Portuguese one, and reading only the English
2267
+ spelling would make every gotcha in such a project invisible to the index — the exact failure
2268
+ hard rule 38 exists to catch, reached by "fixing" the parser.
2269
+
2270
+ - `procedure.template.md` and `learning.template.md` declare the frontmatter their output
2271
+ needs, so a new runbook is born discoverable instead of being retrofitted.
2272
+
2273
+ ### Verification
2274
+
2275
+ - `afm-index.sh` 14 → **16** asserts, tools harness **49**, hooks **101**, gate green. Two of
2276
+ the new asserts are structural rather than behavioural: the set of families in the index is
2277
+ compared as a whole, so a refactor of `cmd_triggers` that silently drops one fails; and a
2278
+ procedure reaching the index with a non-empty condition is asserted, because an entry with an
2279
+ em dash where the condition should be looks identical to a family that was never wired.
2280
+
2281
+ ### Known gap
2282
+
2283
+ - **The `learning` entry declares `trigger_path` and nothing new fires it.** `afm-pre-edit.sh`
2284
+ already greps the `Trigger:` line in the body by path match, and duplicating that in the
2285
+ detector would be a second mechanism for one fact (rule 34). The frontmatter serves discovery
2286
+ here; the existing hook still serves firing.
2287
+
2288
+ - **`afm-pre-edit.sh` matches only the English `Trigger:` in gotchas.** The index reads both
2289
+ spellings now; the hook does not. A consumer bootstrapped before the migration gets its
2290
+ gotchas in the index and not at write time.
2291
+
2292
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.9.0-rc.5` → `3.9.0-rc.6`. `/afm:update` adds the
2293
+ frontmatter block to `docs/procedures/` and `docs/learnings/` where it is missing, and never
2294
+ rewrites a `When to run it` bullet the owner wrote.
2295
+
2296
+ ## [3.9.0-rc.5] — 2026-08-27
2297
+
2298
+ ### Added
2299
+
2300
+ - **Every rubric declares its own trigger in frontmatter, and the detector interprets instead
2301
+ of hardcoding.** The keys are flat, because AFM frontmatter is flat and the reader is shell:
2302
+ `fires_on` (`file-shape` | `repo-shape` | `op-call`), `trigger_path`, `trigger_ext`,
2303
+ `trigger_basename`, `trigger_grep`, `trigger_min_lines`, `trigger_max_consumers`,
2304
+ `trigger_new_module`, `trigger_any`, `fires_when`. A new rubric now ships with a trigger and
2305
+ touches no shell.
2306
+
2307
+ This closes a violation of hard rule 34 that release 3.9.0-rc.4 had created: the six
2308
+ detectors were shell functions holding numbers (`3` sites, `150` lines) transcribed by hand
2309
+ from the prose of the rubrics they implement. Two sources of truth for one trigger, in the
2310
+ file that exists to deliver the rubrics.
2311
+
2312
+ - **`afm-index.sh --triggers`** — the cheap index for ordinary research, not for a hook. The
2313
+ hook answers "does this rubric fire on the file I am writing"; an agent reasoning about work
2314
+ that has no file yet asks "what does AFM have a trigger for at all", and the only way to
2315
+ answer that used to be reading twelve rubrics. `afm-session-start.sh` names the command once
2316
+ per session, which is what makes it reachable outside a hook.
2317
+
2318
+ ### Fixed
2319
+
2320
+ - **`says:` was renamed `fires_when:`, and the rename was earned by a measurement.** A routing
2321
+ experiment gave one arm the 417-token index and another the 13,152 tokens of rubric bodies,
2322
+ over five scenarios. The cheap arm routed correctly in 4 of 5 — and in `when-to-create-lib`
2323
+ it named the right rubric and then **advised the opposite of what that rubric says**, because
2324
+ the agent read the one-line value as the rubric's guidance instead of as a pointer.
2325
+
2326
+ `tools/README.md` had already written the rule this broke: *"There is no `summary:` key on
2327
+ purpose — a summary is a derived duplicate, and when it goes stale the reader believes a
2328
+ wrong fact and never opens the file to find out."* A field named `says` invites exactly that
2329
+ reading. Every value is now a condition with no verdict in it, and the index output opens
2330
+ with `# these are TRIGGERS, not decisions. Open the named file to decide.` — asserted,
2331
+ because the failure existed before the line did.
2332
+
2333
+ - **`solid-triggers` declared only its SRP trigger.** The fifth scenario was an OCP case (a
2334
+ provider `switch` spread across two files) and the index simply did not contain that shape,
2335
+ so no agent could route to it. A rubric with five principles needs a trigger for each one
2336
+ that has a shape. `trigger_any: yes` makes the conditions a disjunction — SRP fires on the
2337
+ file NAME and OCP on the CONTENT, and requiring both would mean neither ever fires.
2338
+
2339
+ - **Re-tested, both defects gone.** Same two scenarios, after the change: correct rubric,
2340
+ guidance judged `equivalent` to the full-bodies arm, and both agents reported
2341
+ `read_index_as_advice: false`.
2342
+
2343
+ ### Measured
2344
+
2345
+ - **The honest saving is about 8x, not 33x.** The index alone is 33x smaller than the bodies,
2346
+ but in the fixed round both agents did what the design intends: read the index, then open the
2347
+ ONE rubric it named. That is ~1,513 tokens against ~13,152 — and it is the correct number to
2348
+ quote, because the alternative reading (417 tokens, file never opened) is precisely the
2349
+ failure this release removed.
2350
+
2351
+ - **Four other families were surveyed for the same technique and all four say no**, with
2352
+ different reasons. `gotchas` (26): half have a path trigger and half have no path at all —
2353
+ a spoken cue, a runtime 403, an agent-output signal — so declaring keys would either split
2354
+ the family in two or fabricate mechanical confidence that dilutes what rubrics and guardrails
2355
+ earned by being genuinely mechanical. `learnings` (1): nothing to index across, and the
2356
+ trigger carries an open clause no glob captures without silently narrowing it.
2357
+ `procedures` (3): two fire on an event in a DIFFERENT repository or on release cadence, and
2358
+ the third's trigger is already hard rule 30's, owned elsewhere. `hard rules` (13): already
2359
+ carry a versioned runner in the gate; frontmatter would be a third representation.
2360
+
2361
+ The technique fits when the trigger is "the file I am about to write matches a pattern".
2362
+ Rubrics and guardrails are the two families that satisfy that. The others answer a different
2363
+ question, and forcing the shape on them would cost more than it returns.
2364
+
2365
+ ### Verification
2366
+
2367
+ - `afm-index.sh` 10 → **14** asserts, `afm-detect.sh` **23**, tools harness **49**, hooks
2368
+ **101**, all at 0 fail. Repo parity held exactly through the rewrite: the same 33 findings
2369
+ before and after the detector stopped hardcoding. Per-file cost measured at **48-53 ms**,
2370
+ inside the hook budget.
2371
+
2372
+ - Hard rule 39 now covers the rubric contract: no `fires_on:` in the closed vocabulary and no
2373
+ `fires_when:` fails the gate, so a rubric cannot be born invisible to the index.
2374
+
2375
+ - Three parser defects were caught by the asserts on the way, all of the same family — the data
2376
+ and the parser disagreeing. YAML-style escapes read raw (`\\(` became an empty group);
2377
+ `fm()` truncating prose at the first comma because it was built for lists; and
2378
+ `grep -c '^rubric\t'` counting zero because `\t` is not a tab escape in a POSIX pattern.
2379
+ **That last one reads exactly like "no rubric declares a trigger"**, and would have been
2380
+ believed.
2381
+
2382
+ ### Known gap
2383
+
2384
+ - **The `op-call` rubrics have no mechanical check that their caller still calls them.** Five
2385
+ rubrics declare `called_by: [...]` and nothing verifies the named op still reads the file —
2386
+ which is the same class as the dead parsers found in 3.9.0-rc.2, one level up.
2387
+
2388
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.9.0-rc.4` → `3.9.0-rc.5`. `/afm:update`
2389
+ materializes the rubric frontmatter and both tools through the existing rows.
2390
+
2391
+ ## [3.9.0-rc.4] — 2026-08-27
2392
+
2393
+ ### Added
2394
+
2395
+ - **Six more rubrics gained a code-shape detector**, so every rubric in the catalogue now has a
2396
+ delivery. `afm-detect.sh` grew `validation-boundary` (a schema parser inside a `domain/`
2397
+ path), `error-classification` (a transport error type or a bare `Error` raised from the
2398
+ domain), `enum-vs-union-vs-branded` (a TS `enum` declaration, or two adjacent `string`
2399
+ parameters — the rubric's own `copy(from, to)` example), `solid-triggers` (SRP: a file named
2400
+ for a manager, a utils bag, or two responsibilities), `when-to-create-lib` and
2401
+ `when-to-create-module`.
2402
+
2403
+ - **`--rubrics`**, the repo-wide uncapped report, next to the per-file `--for` the hook calls.
2404
+
2405
+ ### Fixed
2406
+
2407
+ - **The gap was 6, not 11.** Release 3.8.0-rc.2 recorded "the other 11 rubrics still have no
2408
+ trigger". Five of those eleven were never untriggered: they have a **caller**, which is the
2409
+ other valid form of delivery. `failure-classification` is read by `ops/remediate.md`,
2410
+ `negative-filters` by `remediate` and `reconcile`, `episodic-vs-semantic-boundary` by
2411
+ `reconcile` and `reflect`, `template-vs-streaming-precedence` by `export-template` and
2412
+ `reconcile`, `when-to-evolve-methodology` by `ops/evolve.md`. An op that reads a rubric in
2413
+ order to decide IS that rubric being delivered. Counting a listing in `README.md` and a
2414
+ caller in an op as the same thing is what produced the wrong number.
2415
+
2416
+ ### Verification
2417
+
2418
+ - **Two orphaned self-tests were never being run, and that is why the detector could rot.**
2419
+ The tools harness named its tools one by one, so `afm-detect.sh` (23 asserts) and
2420
+ `afm-index.sh` (10) were each shipped with a self-test that no harness ever executed — 33
2421
+ asserts that reported nothing because nothing asked them. The harness now DISCOVERS every
2422
+ tool carrying `--self-test` instead of listing them, which is the only version of this fix
2423
+ that survives the sixth tool. It goes 47 → **49** at the harness level, and the 33 asserts
2424
+ underneath now actually run.
2425
+
2426
+ - The detector self-test goes 5 → **23**, the hooks harness 98 → **101**, both at 0 fail.
2427
+ Every detector carries a negative assert beside its positive one, because a detector that
2428
+ fires on an ordinary file makes the whole channel ignorable — and an ignored channel still
2429
+ looks like coverage.
2430
+
2431
+ - **Two measured traps, both self-tested now.** The lib row reads "more than ~150 lines of pure
2432
+ logic **coupled to one consumer**", and shipping the size half alone produced **48 hits over
2433
+ 426 source files** here, almost all of them large modules with many importers. Adding the
2434
+ consumer count brought it to 17. Inside that fix, a second trap: the count was anchored at
2435
+ the basename, and TypeScript under NodeNext writes `from "./mod.js"` for a `.ts` file, so it
2436
+ matched nothing and reported "0 consumer" for every file in a real package. **A filter that
2437
+ always answers zero is not a filter**, and it fails in the direction that looks like it works.
2438
+
2439
+ - One assert in the hooks harness failed for the same reason a gate failed earlier in this
2440
+ release line: it counted every occurrence of `docs/rubrics/`, and the header of the injected
2441
+ block quotes that path itself. **A check over a literal needs a positional anchor when the
2442
+ text around it discusses the literal.** Third time this exact shape has been paid for here.
2443
+
2444
+ ### Known gap
2445
+
2446
+ - **Noise is bounded but not measured in use.** 33 findings across 426 source files in this
2447
+ repository, capped at 2 per file. Whether that reads as signal at the moment of an edit is a
2448
+ judgment only real sessions can return.
2449
+
2450
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.9.0-rc.1` → `3.9.0-rc.4`. `/afm:update`
2451
+ materializes the widened `docs/tools/afm-detect.sh` through the existing `docs/tools/` row.
2452
+
2453
+ ## [3.9.0-rc.3] — 2026-08-27
2454
+
2455
+ ### Changed
2456
+
2457
+ - **The 7 Portuguese-named gate runners were renamed.** The rule NUMBER is the identity and it
2458
+ never moved; only the slug did. The 5 runners already in English were not touched.
2459
+
2460
+ | before | after |
2461
+ | --- | --- |
2462
+ | `13-segredos.sh` | `13-secrets.sh` |
2463
+ | `21-idioma.sh` | `21-code-language.sh` |
2464
+ | `34-fonte-unica.sh` | `34-single-source.sh` |
2465
+ | `35-portao-completo.sh` | `35-gate-complete.sh` |
2466
+ | `36-path-absoluto.sh` | `36-absolute-path.sh` |
2467
+ | `37-idioma-prosa.sh` | `37-prose-language.sh` |
2468
+ | `38-marcador-de-parser.sh` | `38-parser-marker.sh` |
2469
+
2470
+ 19 references moved across 6 files. Nothing about the gate's behaviour changed: the
2471
+ aggregator discovers `rules/*.sh` and parses the leading number, and the ratchet is keyed by
2472
+ rule and region, never by file name.
2473
+
2474
+ - **The 3.9.0-rc.2 "Known gap" reasoning was wrong, and this release says so rather than
2475
+ editing it.** That entry argued the rename would leave historical CHANGELOG links dead by
2476
+ construction, because history is not editable. The distinction it missed: a dead POINTER is a
2477
+ defect whatever the age of the prose around it, while a NAME that a sentence makes a claim
2478
+ about is content. So the paths moved everywhere, no sentence was reworded, and the one place
2479
+ where an old name is the SUBJECT of a claim — the rc.2 gap entry itself, which says the names
2480
+ are still Portuguese — keeps the old spelling. A path-only rewrite there would have produced
2481
+ a sentence saying the names are still Portuguese while naming the English ones.
2482
+
2483
+ That single spot is also what the three verification lenses did not catch. All three came back
2484
+ clean, correctly: the edit WAS path-only, so a collateral-damage lens looking for reworded
2485
+ prose had nothing to find. The defect was semantic, and no lens was asked about meaning.
2486
+
2487
+ - **Consumer-noop.** `test/` does not travel and no consumer materializes a runner. The
2488
+ watermark stays at `3.9.0-rc.1`.
2489
+
2490
+ ## [3.9.0-rc.2] — 2026-08-27
2491
+
2492
+ ### Fixed
2493
+
2494
+ - **Three dead mechanisms, found by translating the gates that were exempt from finding them.**
2495
+ Rule 38 catches "the data was translated and the parser was not". `test/` was exempt from it
2496
+ by an explicit decision — those gates speak to the maintainer and are not distributed
2497
+ surface. The operator overturned that on 2026-08-27: code, a test, and a comment are English,
2498
+ and no request overrides it. Lifting the exemption and translating the 19 scripts turned up
2499
+ three parsers left on a spelling nothing produces any more:
2500
+
2501
+ 1. **Rule 34's check (a) had matched zero files since the english-first migration.** It
2502
+ grepped the Portuguese `princípio invariante #` while the ops had become
2503
+ `invariant principle #`. It looked healthy because the ratchet still recorded 6 and 0 is
2504
+ under 6 — **a dead check and a satisfied budget are the same shape from outside.** With
2505
+ the right spelling the count is 6 again, the number the debt entry always described.
2506
+ 2. **The plugin-effect boundary in `refs-lint.sh` extracted the empty string from every op.**
2507
+ The `sed` matched `**Forma** — efeito:` while the check immediately above already required
2508
+ the English `> **Form** —`. This is the guard that no op declares an effect on the plugin —
2509
+ a bright line, silently unguarded. Revived, and it passes clean.
2510
+ 3. **`update-rehearsal.sh` asserted that a Portuguese heading was absent** from hook output
2511
+ that had said `Current protocol` for releases. The assert could only pass.
2512
+
2513
+ A fourth was found next to them, of a different kind: `skills-lint.sh --self-test` had been
2514
+ red since commit `4bb5d95` because it named a real debt row that the same commit fixed. **No
2515
+ gate ran it.** The fixture is now synthetic — an assert about the LOGIC of `is_known_debt`
2516
+ must not depend on what the repo currently owes — and rule 31's runner now runs the self-test
2517
+ before the lint, so it cannot rot in silence again.
2518
+
2519
+ - **`refs-lint.sh` is green for the first time.** `interview/triggers.md` (104 lines, 5
2520
+ sections) had no scope indicator.
2521
+
2522
+ ### Changed
2523
+
2524
+ - **`test/` is no longer exempt from rule 38**, and `ste-batch-check.sh` no longer carries its
2525
+ own Portuguese dictionary — it calls `tools/afm-lang.sh`, so it and rule 37 can no longer
2526
+ disagree about the same file (rule 34). Its old dictionary included words the measured list
2527
+ deliberately excludes.
2528
+
2529
+ - Three exemptions remain, and each one is data rather than instruction: the multibyte fixture,
2530
+ the Portuguese paragraph that is the INPUT of the new language assert, and the dictionary of
2531
+ `afm-lang.sh` itself. Translating that last one would turn rule 37 off entirely.
2532
+
2533
+ ### Known gap
2534
+
2535
+ - **The runner file names are still Portuguese** (`13-segredos.sh`, `37-idioma-prosa.sh`, and
2536
+ the rest). Renaming them is mechanical but it breaks the links in `docs/afm.md` and leaves
2537
+ every historical CHANGELOG entry pointing at a name that no longer exists — history is not
2538
+ editable, so those links would stay dead by construction. It is a separate decision, not a
2539
+ side effect of this one.
2540
+
2541
+ - **Consumer-noop.** `interview/` is read from the plugin and never materialized, and `test/`
2542
+ does not travel. The watermark stays at `3.9.0-rc.1`.
2543
+
2544
+ ## [3.9.0-rc.1] — 2026-08-27
2545
+
2546
+ ### Added
2547
+
2548
+ - **`tools/afm-lang.sh` — the language rule, moved to the moment of writing.** Hard rule 37
2549
+ said the prose of an artifact is English and held a runner that ran at commit time over
2550
+ `*.md` alone. The rule was never wrong. It arrived after the write.
2551
+
2552
+ Measured 2026-08-27, and the measurement is this session: the operator asked in Portuguese,
2553
+ the agent wrote the artifact in Portuguese, and the block came minutes later from the commit
2554
+ gate. **A request made in Portuguese is not a request FOR Portuguese** — that conflation is
2555
+ the failure mode, and no amount of restating the rule in a document had removed it.
2556
+
2557
+ The tool holds the measurement: density of Portuguese function words in prose, the threshold
2558
+ of 1.0 measured in 2026-08 between an English population that peaks at 0.69 and a Portuguese
2559
+ one that bottoms at 1.52. It answers a verdict AND the tier the text belongs to.
2560
+
2561
+ - **Rule 37 gained two tiers, and they are not the same strength.** Code, a test, and a
2562
+ comment are **inalienable** — no explicit request overrides them, which restates rule 21 for
2563
+ the surface rule 21 had left in judgment. A **document is English by default and the
2564
+ operator may override it explicitly**, because a doc has one human audience and that choice
2565
+ is theirs. The override has to be stated, never inferred from the language of the request.
2566
+
2567
+ ### Changed
2568
+
2569
+ - **`afm-pre-edit.sh` runs the language verdict before the `docs/` guard** — the only family
2570
+ that does. Every other one is about product code, so skipping meta-work is right for them;
2571
+ the language rule is broken precisely inside a document. The line names the tier, so the
2572
+ agent is told which of the two it is looking at.
2573
+
2574
+ - **`test/rules/37-prose-language.sh` delegates to the tool instead of holding a copy** of the
2575
+ threshold (rule 34). Two copies of a measured threshold drift apart in silence, and the
2576
+ commit gate would then contradict the hook that had just approved the write. Proved
2577
+ behaviour-preserving: the same 31 files, byte for byte, before and after the delegation.
2578
+
2579
+ The delegation had one defect on its first run and the calibration caught it: `if ! cmd`
2580
+ cannot tell an accusation (exit 1) from an abstention (exit 2, text too short to be a
2581
+ ratio), and collapsing the two reported **83 extra files** — every short eval fixture.
2582
+
2583
+ ### Verification
2584
+
2585
+ - The tools harness goes 43 → **47**, the hooks harness 94 → **98**, both at 0 fail. The
2586
+ asserts that carry weight: a Portuguese doc under `docs/` gets the line at all (it is past
2587
+ the guard that used to eat it), code and doc get *different* tiers, English stays silent,
2588
+ and a `docs/` write with no content is still a no-op — that last one fails if the language
2589
+ block is ever moved below the guard.
2590
+
2591
+ ### Known gap
2592
+
2593
+ - **Rule 37 is still local to this repository.** Promoting it into `core/afm.md`, where every
2594
+ consumer would inherit it, is a methodology change under hard rule 11 and belongs to its own
2595
+ decision, not to the release that built the mechanism.
2596
+
2597
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.8.0-rc.2` → `3.9.0-rc.1`. `/afm:update`
2598
+ materializes `docs/tools/afm-lang.sh` through the existing `docs/tools/` row.
2599
+
2600
+ ## [3.8.0-rc.2] — 2026-08-27
2601
+
2602
+ ### Fixed
2603
+
2604
+ - **The failure capture named runners, and a runner is what changes per project.** The oracle
2605
+ guard in `afm-capture-failure.sh` was a closed list of literal toolchain strings
2606
+ (`npm test`, `jest`, `pytest`, `go test`, `tsc`, `eslint`, `cargo build`) with `exit 0` on
2607
+ default. The closure was right about noise and wrong about coverage.
2608
+
2609
+ Measured across 24 AFM projects on 2026-08-27: **4 failure records in total**, and **no
2610
+ signature ever reached the 2 occurrences hard rule 19 requires**. So `remediate` had never
2611
+ had valid input anywhere, and the three artifact families that only it can create were never
2612
+ born: **5 procedures, 1 learning, 1 guardrail across 24 projects** — against 395 gotchas and
2613
+ 261 ADRs, and every one of those 7 hand-made in the AFM repository itself. The families that
2614
+ work are the ones whose creation does not route through this pipe.
2615
+
2616
+ In one monorepo, 3 of its 4 scripts matched nothing: `turbo run test`, `biome check .` and
2617
+ `bun test` were invisible, while `turbo run typecheck` matched only by the accident of
2618
+ containing the substring.
2619
+
2620
+ The guard now matches the **verb**, which is stable across toolchains, instead of the runner.
2621
+ Quoted text is stripped first, so `git commit -m "fix the test"` is not a test oracle, and a
2622
+ deny-list keeps reader commands (`grep`, `cat`, `--help`) out. `bun`, `deno`, `cargo clippy`,
2623
+ `rake`, `mix`, `make` and any `<runner> run <verb>` now land in the right bucket.
2624
+
2625
+ ### Added
2626
+
2627
+ - **`tools/afm-detect.sh` — delivery for the rubrics whose trigger is a code shape.** Every
2628
+ other family AFM injects at write time matches on a path. The rubrics have none:
2629
+ `when-to-create-dsl.md` fires on "3 or more call sites repeating the same fixed-order
2630
+ sequence", which no path match can see. Measured before this release:
2631
+ `grep -c rubric afm-pre-edit.sh` returned **0**, and the only mention of rubrics in the whole
2632
+ runtime was one clause of one SessionStart bullet — a pointer, not a trigger. The best
2633
+ content in the framework reached the agent never.
2634
+
2635
+ The detector finds repeated call sequences across files, filtering test paths, generated
2636
+ code and assertion vocabulary. On one real repository: 173 raw candidates fell to **39** after
2637
+ filtering, in **89 ms**, and the survivors are genuine — a form-action wiring repeated across
2638
+ 7 components, the `addEventListener`/`removeEventListener` pair across 6, zod schema
2639
+ construction across 11.
2640
+
2641
+ `afm-pre-edit.sh` now injects **one line** when the file being edited is one of the sites,
2642
+ naming the sequence and the rubric. It names the rubric rather than quoting it: the point is
2643
+ to let the agent open the decision table, and a hook that pastes the table has defeated
2644
+ itself.
2645
+
2646
+ - 10 asserts in the hooks harness (84 → **94**): the verb guard across six toolchains, the
2647
+ three adversarial non-oracles, the rubric reaching a file that is a site, and — the one that
2648
+ matters for noise — a file that is *not* a site getting nothing.
2649
+
2650
+ ### Known gap
2651
+
2652
+ - **One detector, one rubric.** The other 11 rubrics still have no trigger. Generalizing the
2653
+ detector before a second real shape exists would be inventing the abstraction (YAGNI); the
2654
+ next shape decides the interface.
2655
+
2656
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.8.0-rc.1` → `3.8.0-rc.2`. `/afm:update`
2657
+ materializes `docs/tools/afm-detect.sh`.
2658
+
2659
+ ## [3.8.0-rc.1] — 2026-08-27
2660
+
2661
+ ### Added
2662
+
2663
+ - **Frontmatter across the methodology, and an index that reads it.** All 62 files under
2664
+ `ops/`, `core/`, `rubrics/`, `templates/`, `templates/protocols/` and
2665
+ `interview/` now carry frontmatter. `tools/afm-index.sh` answers "which file matters for
2666
+ what I am doing" from that metadata alone: `--writes`, `--composes`, `--used-by`,
2667
+ `--orphans`, `--kind`, `--for`, plus `--self-test` with 10 asserts.
2668
+
2669
+ Measured on the 21 ops: the whole index is **314 tokens** against **58,839 tokens** of
2670
+ bodies — 0.5%. "Who writes `tasks.md`" costs 83 bytes through the index; guessing at it
2671
+ costs about 11.6k tokens of speculative reading.
2672
+
2673
+ Before this, frontmatter existed only where a machine already read it — 23/23 skills,
2674
+ the guardrails, the session records. The files an agent must decide whether to open had
2675
+ none: `ops/` was 0 of 21. The retrieval key was already there in four dialects (a prose
2676
+ `Trigger:` in gotchas, the same in learnings, real frontmatter in guardrails,
2677
+ `when_to_use` in the skills), so this consolidates as much as it adds.
2678
+
2679
+ - **Hard rule 39** — an indexed file declares a `kind:`, an op declares `writes:` and
2680
+ `composes:`, and a `composes:` must resolve to a real op. Runner:
2681
+ `test/rules/39-frontmatter-ops.sh` in the AFM repository. It found a real gap on its
2682
+ first run: `guardrail.template.md` had frontmatter and no `kind:`.
2683
+
2684
+ ### Decided
2685
+
2686
+ - **No `summary:` key.** Every key is a property OF the file, so no edit to a body can make
2687
+ its frontmatter false. A summary is a derived duplicate; when it goes stale the reader
2688
+ believes a wrong fact and never opens the file to find out, because skipping the file was
2689
+ the whole point. `used_by` is a fact about the rest of the tree, so it is derived on
2690
+ every call rather than stored, for the same reason the index itself is never written to
2691
+ disk.
2692
+
2693
+ ### Fixed
2694
+
2695
+ - `templates/guardrail.template.md` gained `kind:` and `renders_to:`, keeping the
2696
+ `id`/`origin`/`surface`/`trigger`/`cost` keys it already had.
2697
+
2698
+ ### Known gap
2699
+
2700
+ - **Nothing calls the index yet.** Wiring it into `afm-pre-edit.sh` was built and then
2701
+ reverted: that hook exits on `*/docs/*` by design, and every path an op writes to is
2702
+ under `docs/`, so the branch was dead on arrival. Until a caller exists this is a
2703
+ documented step rather than a mechanism. Rule 39 keeps the metadata true; it does not
2704
+ make anything consult it.
2705
+
2706
+ ### Found
2707
+
2708
+ - `templates/protocols/p2.md` is referenced by nothing, while `p1.md` and `p3.md` are.
2709
+ Surfaced by `--orphans` as a byproduct. Not changed — it may be deliberate.
2710
+
2711
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.7.0-rc.2` → `3.8.0-rc.1`. `/afm:update`
2712
+ materializes `docs/tools/afm-index.sh`. The frontmatter itself rides on files the
2713
+ consumer reads from the plugin, so nothing else changes in the instance.
2714
+
2715
+ ## [3.7.0-rc.4] — 2026-08-26
2716
+
2717
+ ### Fixed
2718
+
2719
+ - **The health cache served the answer from before the bump.** `afm-health.sh` keys its daily
2720
+ cache by project and day. A bump is exactly when vital sign A3 has something new to say, so
2721
+ the operator asked on the one day it mattered and read yesterday — the `3.7.0-rc.3` fix was
2722
+ invisible in the same session that shipped it. The watermark is now part of the key, so a
2723
+ bump invalidates the cache the moment it lands. Measured with the cache warm: the reported
2724
+ watermark follows the file inside one session.
2725
+
2726
+ - **This release exists because hard rule 30 refused the amend, and it was right.** Under
2727
+ `git commit --amend` the bumps already sit in `HEAD`, so the index does not carry them and
2728
+ the runner reported a surface change with no bump. A change to `hooks/` is installable
2729
+ surface and earns its own version.
2730
+
2731
+ - **Consumer-noop.** `hooks/` is read from the plugin, never materialized. The watermark stays
2732
+ at `3.7.0-rc.2`.
2733
+
2734
+ ## [3.7.0-rc.3] — 2026-08-26
2735
+
2736
+ ### Fixed
2737
+
2738
+ - **The watermark stopped moving, and vital sign A3 went blind.** `CONSUMER_VERSION` sat at
2739
+ `3.6.0-rc.6` while `VERSION` reached `3.7.0-rc.2`. A3 in `afm-health.sh` compares the
2740
+ instance against the watermark, not against `VERSION`, so every instance pinned at
2741
+ `3.6.0-rc.6` or later was told there was no drift — and nobody was told to run
2742
+ `/afm:update`. The four releases skipped were `3.6.0-rc.10`, `3.6.0-rc.11`, `3.7.0-rc.1`
2743
+ and `3.7.0-rc.2`, and two of them carry the largest consumer-facing delta the plugin ever
2744
+ shipped: `tools/` and `tools/gate/`, both declared new in `ops/update.md`. The gate the
2745
+ previous two releases went to the trouble of distributing reached no consumer at all.
2746
+ The four entries now declare their consumer impact and the watermark moves to
2747
+ `3.7.0-rc.2`, which is the newest release with materializable content.
2748
+
2749
+ ### Added
2750
+
2751
+ - **Hard rule 30 now checks the declaration, not only the three files.** The declaration
2752
+ discipline was documented in the maintainer runbook, and it held for about 24 releases
2753
+ before it lapsed in silence four times in a row. Hard rule 35 asks that a documented human
2754
+ step with a 0/1 trigger get a versioned runner, and this one has a clean trigger inside the
2755
+ repository: a staged bump of `VERSION` whose release block declares neither
2756
+ `Consumer-facing` nor `Consumer-noop` is a violation, and a block that declares
2757
+ `Consumer-facing` without `CONSUMER_VERSION` in the same index is a violation. The
2758
+ objection that kept `marketplace-sync.sh` out of the gate does not apply — that one reads
2759
+ `~/.claude/plugins/cache`, which is machine state, while both files here are versioned.
2760
+
2761
+ - **Consumer-noop.** The runner is a repository-level gate in the AFM project. It is not
2762
+ materialized, and nothing in `<consumer>/docs/` changes. The watermark stays at
2763
+ `3.7.0-rc.2`.
2764
+
2765
+ ## [3.7.0-rc.2] — 2026-08-26
2766
+
2767
+ ### Fixed
2768
+
2769
+ - **`--repo` was decorative.** The aggregator accepted the flag, and never entered the
2770
+ directory. `git ls-files` answers about the CURRENT directory, so `scope_all` and
2771
+ `scope_files` measured whatever tree the operator happened to stand in, and reported it under
2772
+ the target's name. Called from the AFM repository with `--repo <other project>`, the gate
2773
+ returned findings in `plugins/afm/`. From a subdirectory of the right repository it would
2774
+ silently measure only that subdirectory — a false negative in exactly the class `scope.sh`
2775
+ warns about. The aggregator now enters `$REPO` before it runs anything, after making every
2776
+ path absolute. A regression assert fails when the `cd` is removed.
2777
+
2778
+ ### Verified
2779
+
2780
+ - **The refutation of ADR 0017 was tested, and did not refute.** Four unrelated repositories on
2781
+ one machine, none of them AFM consumers, three plausible TypeScript rules, nothing written
2782
+ into any of them. All four started red, `--baseline` recorded debt between 0 and 26 findings,
2783
+ and all four ended green. That test is what found the `--repo` defect above.
2784
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.6.0-rc.6` → `3.7.0-rc.2`. The fix is inside
2785
+ `tools/gate/guardrails.sh`, which the instance materializes — a consumer that copied the
2786
+ rc.1 aggregator has the defective one.
2787
+
2788
+ ## [3.7.0-rc.1] — 2026-08-26
2789
+
2790
+ ### Added
2791
+
2792
+ - **`tools/gate/` — the plugin distributes the gate, not only a description of it.** The
2793
+ aggregator (`guardrails.sh`), the two sourceable libraries (`lib/scope.sh`, `lib/ratchet.sh`),
2794
+ the ratchet seed and an empty `rules/` now materialize into `<consumer>/docs/tools/gate/`.
2795
+ Before this release `ops/rule.md` carried six mature steps to turn a hard rule into an
2796
+ executed guardrail and no executable code stood behind any of them.
2797
+ - **`--baseline`, the mode that makes the export survive.** It measures HEAD, records what it
2798
+ finds as declared debt, and says out loud which rules it skipped. A gate turned on in a
2799
+ repository that already breaks the rule starts red, and a gate that starts red is disabled the
2800
+ next day — taking every other rule in the same aggregator with it. The mode appends only, and
2801
+ never touches a rule that already has a row, because a measured region boundary is the one
2802
+ thing in the ratchet that cost real work.
2803
+ - **`AFM_GATE_LIB`**, exported by the aggregator to every runner. A runner resolves its library
2804
+ through the variable and never through a fixed path, so the same runner works in a project
2805
+ that keeps the gate under `docs/tools/gate/` and in one that keeps it somewhere else.
2806
+ - **Path parameterization:** `--repo`, `--rules`, `--ratchet`, `--afm-doc`. A consumer that took
2807
+ the materialized layout passes nothing; a project with its own layout passes flags.
2808
+ - **`--install` writes the hook it arms.** It creates `.githooks/pre-commit` when absent, then
2809
+ sets `core.hooksPath`. Arming a hooks directory that holds no hook was a silent no-op.
2810
+
2811
+ ### Fixed
2812
+
2813
+ - **`templates/guardrail-runner.template.md` shipped a dead premise and a dead path.** It
2814
+ claimed `framework-template/` is Markdown by design — untrue since 3.6.0-rc.11 — and told the
2815
+ consumer to source `test/lib/scope.sh`, a file that exists only in the AFM repository. A
2816
+ runner written from the template died at its own `source` line.
2817
+ - **`afm-pre-commit.sh` and `afm-health.sh` could not see a materialized gate.** Both looked at
2818
+ `test/` only, so a consumer with the gate under `docs/tools/gate/` was reported as having no
2819
+ gate — the same silence the health check exists to break.
2820
+
2821
+ ### Decided
2822
+
2823
+ - **The machine ships; the rules do not.** A runner encodes one project's hard rule (31 is about
2824
+ `SKILL.md`, 32 about `${CLAUDE_SKILL_DIR}`, 33 about `hooks/`). Shipping runners would make
2825
+ every consumer inherit rules nobody chose. See `docs/adr/0017-*` in the AFM repo.
2826
+ - **No standalone `pre-commit` file in the distribution.** `--install` already writes it, and a
2827
+ second copy of the same three lines is the drift rule 34 exists to prevent.
2828
+ - **Consumer-facing.** `/afm:update` materializes `docs/tools/gate/`. The watermark moves in
2829
+ the rc.2 entry, which is the newest consumer-facing release of this train.
2830
+
2831
+ ## [3.6.0-rc.11] — 2026-08-25
2832
+
2833
+ ### Added
2834
+
2835
+ - **`tools/` — the first executable component the plugin distributes.** Until this
2836
+ release `framework-template/` held zero `.sh` files: every gate a consumer had was one
2837
+ it built by hand. The guardrails research of 2026-08-25 measured that gap and named it
2838
+ the real one — the rules were never the problem, the distribution was. Two tools ship
2839
+ now, both read-only, both without a dependency beyond `python3`:
2840
+ - **`tools/afm-cost-ledger.sh`** (level 0) reads the session transcripts the CLI
2841
+ already writes and prices each session: tokens by model, the time window, the branch.
2842
+ - **`tools/afm-outcome-join.sh`** (level 1) joins those rows to git, taking the gate
2843
+ verdict inside an **ephemeral worktree at each commit**. It answers "did the gate go
2844
+ green without a human repairing it, and at what token cost". A verdict read at `HEAD`
2845
+ answers a different question and cannot tell a commit that was born green from one
2846
+ somebody repaired later.
2847
+ - **`tools/README.md`** carries the contract, and **`tools/test/run.sh`** carries 21
2848
+ asserts plus fixtures. It says out loud when `shellcheck` is absent: a silent skip is
2849
+ how a repository believes it has static coverage that it does not have.
2850
+
2851
+ ### Fixed
2852
+
2853
+ - **Five measured traps in the transcript data**, each of which produces a plausible
2854
+ wrong number. (1) One API response is written as many JSONL lines that repeat the same
2855
+ `usage` object — a naive sum inflated the AFM repo by **2.2x**, so the count
2856
+ deduplicates by `message.id`. (2) `subagent_tokens` in the harness notification is
2857
+ **not** an output count: it is `output + input + cache_creation`, matched to 1.6% on a
2858
+ measured run, so it lives in its own column and is never compared to `out`. (3) That
2859
+ notification covers **workflow runs only** — a plain subagent reports nothing, and
2860
+ 13,175 output tokens were invisible in one session — so subagent cost is summed from
2861
+ the transcripts, which cover both. (4) The subagent `output_tokens` field is reliable
2862
+ on CLI 2.1.238 and 2.1.241 (ratio 0.3–0.8 across 30 runs) and broken on 2.1.245 (ratio
2863
+ 20.4), so a per-run plausibility check marks it `suspect` and the column is read as a
2864
+ floor. (5) A session that *discusses* the format writes the element into its own
2865
+ transcript, and the ledger read its own fixture as a real run; a figure now counts only
2866
+ inside a `queue-operation` line carrying a `<task-notification>`, once per `task-id`.
2867
+
2868
+ ### Decided
2869
+
2870
+ - **No gate over the cost number**, and no ratchet. Cost per session moves with the task,
2871
+ so a threshold would fire for no reason, and a gate that fires for no reason is turned
2872
+ off. The tools report. See `docs/adr/0016-*` in the AFM repo.
2873
+ - **Consumer-facing.** `/afm:update` materializes `docs/tools/`. The watermark moves in the
2874
+ `3.7.0-rc.2` entry.
2875
+
2876
+ ## [3.6.0-rc.10] — 2026-08-25
2877
+
2878
+ ### Added
2879
+
2880
+ - **`core/afm.md` §1.3 — "In a fan-out of agents, the model tier follows the task tier".**
2881
+ A workflow, or any batch of parallel subagents, no longer gives every agent the same
2882
+ model. The light tier takes the mechanical, high-volume stages (search, read, collect,
2883
+ `grep`, extract, label). The capable tier takes the stages where a wrong answer
2884
+ contaminates everything downstream (synthesis, judgment, design, adversarial
2885
+ verification, the final report). The criterion is **cost per unit of risk**: a research
2886
+ agent that misses one file costs one more search, and a synthesis agent that reasons
2887
+ wrong writes the conclusion the human then acts on. The model is set **per agent call**,
2888
+ never once for the whole run, and the tier of each phase goes in the workflow metadata
2889
+ so the choice is auditable. No 0/1 trigger separates a mechanical stage from a synthesis
2890
+ stage, so this is a principle, not a hard rule (#3).
2891
+
2892
+ **Consumer-facing:** a new bullet in `docs/afm.md` §1.3. `/afm:update` materializes it. The
2893
+ watermark moves in the `3.7.0-rc.2` entry.
2894
+
2895
+ ## [3.6.0-rc.9] — 2026-08-24
2896
+
2897
+ > **Five subagents read the plugin as a first-time consumer would. A mechanical sweep then
2898
+ > found what they could not see.** Together they surfaced **14 instances of one failure**:
2899
+ > the data was translated and the parser was not. Three of them had killed a mechanism in
2900
+ > silence. None had ever been caught by a gate.
2901
+
2902
+ ### Fixed — three mechanisms that reported healthy and measured nothing
2903
+
2904
+ - **Vital sign A4 (broken ADR chain) found zero, always.** `afm-health.sh` matched
2905
+ `Substituída por ADR-`, `templates/adr.template.md` offered `Superseded by ADR-NNNN`,
2906
+ and the real ADR wrote `Replaced by [ADR-0005](...)`. **Three spellings, zero agreement.**
2907
+ Canonicalized on `Superseded by`, and the pattern now accepts the markdown-link form the
2908
+ instance actually writes. Counter-proved in four directions: it fires on the bare form and
2909
+ on the link form, and stays silent both on the dead Portuguese spelling and on a healthy
2910
+ chain. `ops/diagnose.md` axis A4 carried the same dead pattern.
2911
+ - **`/afm:restructure` was inert.** Its declared precondition is `git + Bites:` and it prunes
2912
+ by reading `last:`. Both were written as `Mordidas:` / `última:`, which the template never
2913
+ produces. The op could not trigger, and the dead-gotcha prune could never find anything.
2914
+ `ops/gotcha.md` also told the agent to fill the two fields under their dead names.
2915
+ - **The guardrail catalog documented keys nobody reads.** `guardrails/README.md` described the
2916
+ frontmatter as `sinal` / `gatilho` / `superficie` / `parametros`; the real files use
2917
+ `signal:` / `trigger:` / `surface:` / `params:`. A maintainer following the README would
2918
+ write a guardrail the hook never sees. The three guardrail `title:` fields were also still
2919
+ Portuguese — and that is text **injected into the agent's context**.
2920
+
2921
+ ### Fixed — a test suite that was orphaned from the gate and rotted
2922
+
2923
+ `plugins/afm/hooks/test/smoke-v32.sh` is the integration test for hard rules 17-20. **Nothing
2924
+ ran it**, so nobody saw it go red. Three of its asserts matched hook output that had been
2925
+ translated: `AFM ativo` → `AFM active`, `Learnings que casam` → `Matching learnings`,
2926
+ `Handoff pendente` → `Pending handoff`. Now 17/17.
2927
+
2928
+ A fourth assert was worse, because it was **green**: `grep -q 'gatilho(s)'` as a **negative**
2929
+ assert. No hook prints `gatilho` any more, so it passed vacuously — it would have passed with
2930
+ axis A5 completely broken. It now matches the real English output, and breaking A5 on purpose
2931
+ makes it fail.
2932
+
2933
+ - **Rule 33's runner now runs every executable smoke in `hooks/test/`, not only `run.sh`.**
2934
+ A test that no gate runs is indistinguishable from a test that does not exist.
2935
+
2936
+ ### Fixed — the interview would have taught the wrong rules
2937
+
2938
+ - **`interview/bootstrap.md` step 5 offered rule labels that pointed at the wrong rules.**
2939
+ Ten of sixteen were wrong. The worst: `13 — No dead feature flag`, where **real rule 13 is
2940
+ "tokens and secrets never leak"** — a user opting out of what they read as a feature-flag
2941
+ rule would have marked the secrets rule N/A. The table is rebuilt from `core/afm.md § 3`,
2942
+ and now carries the instruction to read the file first and distrust the table on conflict.
2943
+ - **The count is 16 and that was right for the wrong reason.** § 3 numbers slots to 21; 3, 8,
2944
+ 9 and 14 are demoted principles with no trigger, and 21 is INALIENABLE. The doc now says why,
2945
+ and says not to renumber — `diagnose` A5b compares instance against plugin **by number**.
2946
+ - **`interview/refactor.md` said "§ 3 (rules 1-16) → copy from core/afm.md".** That silently
2947
+ drops 17, 18, 19, 20 and the INALIENABLE 21. It now says to copy every rule and never a prefix.
2948
+ - **`INALIENÁVEL` vs `INALIENABLE`** in `bootstrap.md`, `ops/diagnose.md` and `ops/rule.md`:
2949
+ `core/afm.md` says `INALIENABLE`, so all three matched zero rules. Effect: the interview would
2950
+ have **offered rule 21 (code in English) as opt-out**, `diagnose` A5b would never report the
2951
+ CRITICAL, and `/afm:rule` would have agreed to delete it.
2952
+ - **D.12 told the agent to edit README sections that do not exist** (`Documentação canônica`,
2953
+ `Doutrina`, `Comandos` — the headings are English), and to paste a Portuguese sentence into an
2954
+ english-first `CLAUDE.md`. Both commit templates and the materialized `## 3.1 Regras
2955
+ forward-only` block were still Portuguese — that block is **written into the consumer**.
2956
+ - **The rubric count was hardcoded, drifted twice** (7 in one place, 8 in another, **12** on
2957
+ disk). The number is gone; the docs now point at `rubrics/`.
2958
+
2959
+ ### Fixed — dead references
2960
+
2961
+ - **Four dead anchors.** `evolve.md#phase-4--aplicação--pós-condições--commit-κ-efetivado` in
2962
+ `ops/{update,generalize,reflect,restructure}.md`. The heading became English; the slug did not.
2963
+ - **`core/afm.md § 2.7` does not exist** (`ops/tasks.md`, `templates/feature-tasks.template.md`
2964
+ — the second **materializes into every consumer**). The heading is `### 7. COMMIT` inside
2965
+ `## 2. Task flow`. `§ 2.1` and `§ 2.2` are real, which is what made `§ 2.7` look plausible.
2966
+ - **15 stale `§` citations** to headings renamed during the migration: `§ O alvo da evolução`,
2967
+ `§ Disciplina de proposta (compartilhada)`, `§ Contrato de supressão`, `§ Instalar o portão`,
2968
+ across `ops/{deliver,diagnose,generalize,restructure,update}.md`,
2969
+ `rubrics/failure-classification.md` and `templates/guardrail.template.md`. Plus
2970
+ `ach.md § convenções` (now `§ 6`) and `evolve.md § Shared proposal discipline` (the heading is
2971
+ `Proposal discipline (shared)`). **`refs-lint.sh` does not see this class** — it resolves file
2972
+ links, not `§` anchors.
2973
+
2974
+ ### Added — hard rule 38, the remediation for the class
2975
+
2976
+ `test/rules/38-parser-marker.sh`. Since the repo is english-first (rule 37), an accented
2977
+ word inside a `.sh`, or inside a backtick span in a plugin `.md`, is a **candidate**. It does not
2978
+ prove drift; it says where drift lives, and a human triages. It complements rule 37, which
2979
+ measures prose **density** in `.md` only and is blind both to shell and to one stray literal.
2980
+
2981
+ **Two implementation notes that are themselves paid bugs.** The prototype reported **0 findings
2982
+ against a corpus with 6 known cases**, twice:
2983
+ 1. It required the literal to sit **inside quotes within a `grep`/`awk`**. Real drift appears as
2984
+ a bare word (`for scope in strict flavored ambos`).
2985
+ 2. It used the range `[A-Za-zÀ-ÿ]`, which is **invalid outside a UTF-8 locale**, and sent the
2986
+ error to `/dev/null`. `grep` aborted and the script printed "0 findings". **A gate that
2987
+ swallows its own error is indistinguishable from a gate that passed** — precisely the bug it
2988
+ exists to hunt. It now pins `LC_ALL=C`, uses explicit alternation, and fails hard.
2989
+
2990
+ Backticks are paired with `awk` on even-indexed fields, never with `` `[^`]*…[^`]*` `` — that
2991
+ pattern also matches the **gap between** two code spans, which is how the first version reported
2992
+ "à la carte" as a Portuguese marker. Six exemptions are justified in the script, including
2993
+ `core/afm.md` (it quotes `criarPedidoComEndereço` as an example of what rule 21 **forbids**) and
2994
+ `ops/deliver.md` (the ephemeral-override phrases are **user speech**, and rule 37 already says a
2995
+ reply follows the speaker's language).
2996
+
2997
+ ### Fixed — `seo` runtime strings, and `ste` enums
2998
+
2999
+ - **23 Portuguese runtime strings in 6 `seo` packages.** Comments and zod descriptions had been
3000
+ migrated; the **output** had not — `gate.ts` (the verdicts that decide whether a page is
3001
+ created), `metadataRules.ts` (what `/seo:audit` reports), `indexingSubmit.ts`, `contentBrief.ts`,
3002
+ `clusterLinks.ts`, `internalLinkSuggest.ts`. The earlier sweep counted **comments**, not string
3003
+ literals, which is why it missed them. Five test assertions matched those strings — including
3004
+ one named *"flags title problems (warn) with PT messages"*. **1731/1731 tests, equal to baseline.**
3005
+ - **`ste` documented enum values that no longer exist.** `rules/README.md` described the scope as
3006
+ `ambos` and the severity as `planejada`; `bin/ste-lint` compares `both` and `planned`. A
3007
+ maintainer following the README would add **a rule that never fires**. `test/run.sh` passed
3008
+ `--scope ambos` literally, exercising a phantom value, and passed anyway because it only checks
3009
+ `rc<=1`.
3010
+ - **`ste-lint`'s real default `--scope both` was undocumented** — its own usage line listed only
3011
+ `strict|flavored`. Measured on `PRINCIPLES.md`: **10 findings** without the flag, **6** with
3012
+ `--scope flavored` (what `SKILL.md` prescribes for prose). The usage now states both defaults.
3013
+
3014
+ ### Fixed — repo gate documentation
3015
+
3016
+ - **Rule 30's prose named `seo`/`learn`; the runner covers four plugins** including `ste`.
3017
+ - **Rule 37's exemption list said two exclusions; the script had three**, and the third
3018
+ (`plugins/ste/rules/*-pt.tsv`) was **dead code** — the scan is `scope_all '*.md'`. Removed.
3019
+ - **`test/lib/scope.sh` claimed diff-rules always carry budget 0.** The ratchet grants rule 33 a
3020
+ budget of 1 for `plugins/seo`. The comment misled anyone reading only it.
3021
+
3022
+ ### Consumer migration
3023
+
3024
+ 1. **`docs/gotchas.md` — rename the incidence field to `**Bites:** N (last: YYYY-MM)`** if your
3025
+ instance uses `Mordidas:`/`última:`. `/afm:restructure` reads the English names to prune dead
3026
+ gotchas; with the old names its precondition never holds and it does nothing, silently.
3027
+ 2. **`docs/adr/*.md` — write `Superseded by ADR-NNNN`** (the markdown-link form works too).
3028
+ `afm-health.sh` A4 reads that literal. `Substituída por` and `Replaced by` are not recognized.
3029
+ 3. **`docs/guardrails/*.md` — the frontmatter keys are `signal:`, `trigger:`, `surface:`,
3030
+ `params:`.** The Portuguese key names were never read by anything.
3031
+ 4. Everything in the rc.7 block still applies to an instance pinned below `3.6.0-rc.1`.
3032
+
3033
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.6.0-rc.7` → `3.6.0-rc.9`.
3034
+
3035
+ ## [3.6.0-rc.8] — 2026-08-24
3036
+
3037
+ > **The changelog itself was still Portuguese, and the section references were dead.**
3038
+ > This release finishes the english-first migration inside the plugin: it rewrites both
3039
+ > changelogs, and it turns "the prose of an artifact is English" into a gate that measures.
3040
+
3041
+ ### Added — hard rule 37, with a measured threshold
3042
+
3043
+ - **Hard rule 37 in `afm.md § 3` + [`test/rules/37-prose-language.sh`](../../../test/rules/37-prose-language.sh).**
3044
+ Rule 21 covers only an identifier or a test name with a non-ASCII character. It is blind to
3045
+ prose: a `.md` written entirely in Portuguese passes it clean. **That is how this repo reached
3046
+ 2026-08 with 175 Portuguese doc files while the gate said green.**
3047
+
3048
+ The signal is the **density** of Portuguese function words in prose, not their presence.
3049
+ Presence is a guaranteed false positive: every doc here quotes `agente` and the old
3050
+ `**Gatilho:**` — literals the gates need to stay Portuguese.
3051
+
3052
+ **The threshold is measured, not chosen.** The English population (n=117) peaks at **0.69**,
3053
+ the Portuguese population (n=29) bottoms at **1.52**, and **1.0** falls in the gap.
3054
+ `MIN_HITS=3` removes the two 2-hit outliers at the top of the English population.
3055
+ The word list deliberately omits `no`, `com`, `de`, `os` and `ou`: `example.com` tokenizes to
3056
+ `com`, which produced a systematic false positive across every `.ts` file on 2026-08-23.
3057
+
3058
+ **The exemption is counter-proved, not assumed.** `plugins/ste/test/fixtures/*-pt.md` is
3059
+ excluded because the `ste` plugin lints Portuguese and those files are the input its test
3060
+ proves something about. Removing the exclusion makes the runner report all three (densities
3061
+ 23.53, 21.43, 16.00). Putting it back silences them. The gate green with the exemption is not
3062
+ the same as the gate blind.
3063
+
3064
+ **31 files are registered as measured debt** in `test/baseline/ratchet.tsv`: 14 in
3065
+ `docs/sessions`, 9 in `test/evals`, 5 at the root, 2 in `docs/research`, 1 in `plugins/seo`.
3066
+ The count can only decrease.
3067
+
3068
+ ### Changed — both changelogs are English, and the markers were renamed
3069
+
3070
+ - **`CHANGELOG.md` and `CHANGELOG-archive.md` rewritten in ASD-STE100 English** — 52 entries,
3071
+ one agent per entry, each one read back against the Portuguese original in git by a second
3072
+ agent that hunts **loss, invention and inversion**. 12 defects were found and fixed. The three
3073
+ that mattered: an **inversion** in the rc.13 dogfooding notes (the English said the threshold
3074
+ belonged to the operator when the source says it belonged to the agent — the opposite of the
3075
+ lesson), an **invented** `### Consumer migration` heading in `3.0.0-rc.1` where the source has
3076
+ none, and a Portuguese `NÃO podar por tamanho` left standing inside an otherwise English bullet.
3077
+
3078
+ - **The consumer-migration heading is now `### Consumer migration`, one spelling.** `ops/update.md`
3079
+ reads it to decide what a consumer materializes. The source had **two** spellings —
3080
+ `### Migração de consumer` (4 times) and plain `### Migração` (9) in the active file. The
3081
+ instruction "never translate this marker" then made verifiers normalize in three different
3082
+ directions, including one `### Migration`. Machine-read markers do not tolerate a synonym.
3083
+ All **40** blocks now carry the single English form, and `ops/update.md` reads that form.
3084
+
3085
+ - **`**Substituições**` → `**Replacements**` and the table schema `Arquivo | De | Para` →
3086
+ `File | From | To`,** with the six application rules in `ops/update.md` renamed to match.
3087
+ A parser and its data renamed in the same commit — the discipline the previous batches broke
3088
+ four times.
3089
+
3090
+ ### Fixed — 15 dead section references
3091
+
3092
+ The batches translated the headings and left the citations pointing at the Portuguese names.
3093
+ `§ O alvo da evolução`, `§ Disciplina de proposta (compartilhada)`, `§ Contrato de supressão`
3094
+ and `§ Instalar o portão` appeared **15 times** across `ops/{deliver,diagnose,generalize,restructure,update}.md`,
3095
+ `rubrics/failure-classification.md` and `templates/guardrail.template.md`. Every one
3096
+ of them told an agent to go read a section that no longer exists under that name. Two more were
3097
+ individually wrong: `ach.md § convenções` (now `ach.md § 6`) and `evolve.md § Shared proposal
3098
+ discipline` (the real heading is `Proposal discipline (shared)`).
3099
+
3100
+ **`refs-lint.sh` does not see this class** — it resolves file links, not `§` anchors.
3101
+ The 17 were found by a one-off script, not by the gate.
3102
+
3103
+ ### Fixed
3104
+
3105
+ - **`refs-lint.sh` was blind to `CHANGELOG-archive.md`.** The exclusions matched `*CHANGELOG.md`,
3106
+ and the archive file created in rc.7 does not match that glob. **10 false FAILs.** One of the
3107
+ three exclusion sites was `grep -v 'CHANGELOG.md'`, where the `.` is a regex wildcard that
3108
+ still failed to match the new name.
3109
+
3110
+ ### Corrected — a number from rc.7 was wrong
3111
+
3112
+ The rc.7 entry said the split preserved "51 entries" and "30 migration blocks". Measured again
3113
+ against `git show HEAD`: **52 entries** (21 active + 31 archived) and **40** migration blocks
3114
+ (13 + 27). The 30 came from counting only the long-form heading and missing the 10 plain
3115
+ `### Migração`. The entry now carries the measured numbers.
3116
+
3117
+ ### Consumer migration
3118
+
3119
+ Nothing beyond the block already written for the whole `3.6.0` line in rc.7. Rule 37 and its
3120
+ runner are a **repo gate of the plugin**; they are not materialized. `CONSUMER_VERSION` stays
3121
+ at `3.6.0-rc.7`.
3122
+
3123
+ ## [3.6.0-rc.7] — 2026-08-24
3124
+
3125
+ > **The changelog got too large to be read, and no summary was safe.** Two ultracode merge
3126
+ > rounds tried to compress it. Both lost measured data. This release moves the old releases
3127
+ > instead. Nothing is compressed, and nothing is lost.
3128
+
3129
+ ### Changed — releases before `3.5.0` moved to `CHANGELOG-archive.md`
3130
+
3131
+ `CHANGELOG.md` had **1832 lines** (~25k words). `ops/update.md` reads it to materialize the
3132
+ deltas of a consumer, so its size is a real cost on every `/afm:update`.
3133
+
3134
+ **The measured reason a summary is not possible.** The 1832 lines hold **322 file paths, 96
3135
+ measured numbers, 50 table rows, 32 migration blocks and 10 arXiv IDs**. There is almost no
3136
+ ornament to cut. Round 1 (effort `low`) invented `## [3.2.0]` release headers — **no minor of
3137
+ this project was ever finalized**, all 51 entries are `-rc`. Round 2 (effort `medium`, with an
3138
+ explicit instruction to never drop a measured number) still lost 4 arXiv IDs, whole `Changed`
3139
+ sections with their file paths, and one migration block.
3140
+
3141
+ **What was done instead:** releases `3.0.0-rc.1` through `3.4.0-rc.7` moved to
3142
+ `CHANGELOG-archive.md` **verbatim**. The active file keeps `3.5.0` and `3.6.0`.
3143
+
3144
+ | file | lines | releases |
3145
+ | --- | --- | --- |
3146
+ | `CHANGELOG.md` | 1051 (−43%) | `3.5.0-rc.*`, `3.6.0-rc.*` |
3147
+ | `CHANGELOG-archive.md` | 802 | `3.0.0-rc.1` … `3.4.0-rc.7` |
3148
+
3149
+ Verified after the split: **52 entries** (was 52) and **40 `### Consumer migration` blocks**
3150
+ across the two files.
3151
+
3152
+ - **`ops/update.md` step 1 must read the archive too** when the version range reaches back
3153
+ before `3.5.0`. The archived releases still carry their migration blocks. An instance pinned
3154
+ to an old version needs them, and missing the archive means the consumer **silently** never
3155
+ receives those deltas.
3156
+
3157
+ ### Fixed
3158
+
3159
+ - **`core/prd.md` line 52 still said `*[A DEFINIR.]*`.** The `[A DEFINIR]` → `[TBD]` rename of
3160
+ rc.1 missed it. `afm-health.sh` counts only `[TBD]` and `[NEEDS CLARIFICATION:`, so a marker
3161
+ in the old spelling is **invisible to the vital signs** — an open decision that never shows.
3162
+
3163
+ ### Consumer migration
3164
+ This block covers the whole `3.6.0` line (`rc.1` … `rc.7`), because the english-first migration
3165
+ landed across the six releases as one change. An instance pinned below `3.6.0-rc.1` applies all
3166
+ of it.
3167
+
3168
+ 1. **`docs/gotchas.md` — rename `**Gatilho:**` to `**Trigger:**` in every entry.** This is the
3169
+ one step that is **not** cosmetic. `afm-pre-edit.sh` greps for `trigger.*` to decide which
3170
+ gotcha to inject at write time. An instance that keeps the old spelling gets **zero gotchas
3171
+ injected, silently, with every gate green**. The same applies to the `trigger:` frontmatter
3172
+ key of `docs/learnings/*.md`.
3173
+ 2. **Replace `[A DEFINIR]` with `[TBD]`** everywhere under `docs/`. `afm-health.sh` counts only
3174
+ `[TBD]` and `[NEEDS CLARIFICATION:`; the old spelling is invisible to the A2 vital sign.
3175
+ 3. **`docs/afm.md § 1.3` — the language principle inverted.** The old text said *"match the
3176
+ language of the file (PT-BR default). Never translate an existing section."* That is now
3177
+ **false**. Every artifact — docs, code, DB, comments, tests — is written in **English under
3178
+ ASD-STE100**. The chat reply follows the language the user speaks; a reply is not an
3179
+ artifact. **The end-user-facing strings of the app are the exception:** the app language is a
3180
+ product decision, it lives in `docs/prd.md`, and it is never forced.
3181
+ 4. **Scope indicators, if your docs use them:** `> **Neste arquivo:**` → `> **In this file:**`,
3182
+ `> **Forma** —` → `> **Form** —`, and `grau de liberdade: alto|médio|baixo` →
3183
+ `degree of freedom: high|medium|low`. Cosmetic in a consumer (the lint that reads them is a
3184
+ repo gate of the plugin, and it does not travel), but keep one spelling per instance.
3185
+ 5. **Do not translate enum literals.** `agente`, `ambos` and `planejada` are matched by string
3186
+ in the gates. Translating the data and leaving the parser behind is the failure class this
3187
+ whole migration produced **four** times. Translate the prose, never the token a `grep` or an
3188
+ `awk` compares against.
3189
+
3190
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.5.0-rc.11` → `3.6.0-rc.7`.
3191
+
3192
+ ## [3.6.0-rc.6] — 2026-08-23
3193
+
3194
+ ### Fixed — `refs-lint.sh` was blind on 41 points
3195
+
3196
+ This is the most costly finding of the migration, and it **did not come from any checker**. It came from
3197
+ running a repo lint that the earlier batches had not run.
3198
+
3199
+ `test/refs-lint.sh` matches **literal Portuguese markers** that the translation changed:
3200
+
3201
+ | the lint matched | the docs now say | effect |
3202
+ | --- | --- | --- |
3203
+ | `^> \*\*Neste arquivo:\*\*` | `> **In this file:**` | 21 files "with no scope indicator" |
3204
+ | `^> \*\*Forma\*\* —` | `> **Form** —` | 21 ops "with no declared shape" |
3205
+ | `grau de liberdade: (alto|médio|baixo)` | `degree of freedom: (high|medium|low)` | degree never extracted |
3206
+ | `*REMOVIDO*` in the exclusion | `**REMOVED**` | a command documented as removed became a "dead command" |
3207
+
3208
+ **And the lint's own self-test failed too**, because its fixtures generate
3209
+ `> **Forma** — grau de liberdade:` while the `sed` already looked for the English. A gate whose
3210
+ self-test fails proves nothing.
3211
+
3212
+ **Drift only the lint would find:** 6 of the 21 ops translated `**Forma**` as `**Shape**` and
3213
+ the other 15 as `**Form**`. Same marker, two translations, no agent seeing both.
3214
+ Normalized to `**Form**`.
3215
+
3216
+ **Measured consequence of the 9% word-count growth:** `PRINCIPLES.md` (119 lines),
3217
+ `ops/remediate.md` (135), and `rubrics/failure-classification.md` (102) crossed the
3218
+ 100-line threshold and started to **require** the scope indicator they lacked.
3219
+ Added to all three. STE bloat is not just token cost — it changes which rule applies.
3220
+
3221
+ The `REMOVED` exclusion accepts **both spellings**: a consumer materialized before
3222
+ 3.6.0 still writes `REMOVIDO`.
3223
+
3224
+ ### Added
3225
+
3226
+ - **Episodic `docs/` in English** (batch B8, 27 files): 15 ADRs, 4 evolution
3227
+ reports, features 001 and 002, and 2 research dossiers.
3228
+
3229
+ ### Fixed during the rewrite
3230
+
3231
+ - **Invented "the team" for the third time** (`adr/0002`), from Portuguese sentences
3232
+ **with no subject** (*"uma melhoria de método é feita"*, *"exige disciplina"*). Portuguese
3233
+ elides the subject, English needs one, and the translator filled the gap with a fact. The owner is
3234
+ **solo**, and the doc itself says so.
3235
+ - **Swapped referent** (`adr/0014`): *"desfazer parte da 001"* — **feature** 001 —
3236
+ became *"Undoing part of ADR 0013"*. In a doc whose job is precise cross-reference.
3237
+ - **Logical inversion** (`evolution/001`): *"exigem exatamente o gate que eu pulei"* became
3238
+ *"require the opposite gate"*. The point of the paragraph is that the required gate and the skipped one are
3239
+ **the same** — the evidence of the agent's non-conformance. "Opposite" destroys the argument.
3240
+ - **`tasks.md` of feature 002 translated the enum literal `agente`** to `agent`. The
3241
+ `test/guardrails.sh` **prints and matches** `agente` (lines 140/142/161). `afm.md` and
3242
+ `plan.md` kept it; only `tasks.md` did not.
3243
+ - **`adr/0011` was left with 3 dead references** to `gotchas.md` section titles that
3244
+ batch B7 translated.
3245
+ - **`evolution/003` lost "uma das TRÊS razões"** — and that count *is* the evidence of the
3246
+ very finding the paragraph records.
3247
+
3248
+ ## [3.6.0-rc.5] — 2026-08-23
3249
+
3250
+ ### Added
3251
+
3252
+ - **The 16 `hooks/` files in English** (batch B6): the 7 hooks, `lib/afm-cadence.sh`,
3253
+ the `README.md`, the 84-assert smoke test, and the container harness. This text enters
3254
+ the context of **every session**, so it is the purest `strict` case in the toolkit.
3255
+
3256
+ ### The verifier said clean and the suite found 9 failures
3257
+
3258
+ This is not a contradiction. These are layers that see different things.
3259
+
3260
+ The adversarial verifier reads **one file at a time** and answers *"did this translation
3261
+ preserve this file?"*. For the 12 hooks the answer was yes, and it was correct: **zero
3262
+ code-change**, no `grep`/`awk` pattern touched, `bash -n` clean on all of them.
3263
+
3264
+ The failures came from **coupling between files**. `test/run.sh` asserted
3265
+ `test("AFM ativo")`, and the hook started to emit `AFM active in this project`. No agent
3266
+ looking at one file in isolation could see this. There were 10 asserts across 8 distinct
3267
+ patterns.
3268
+
3269
+ **This is the same lesson from rc.2, in the good direction.** There, a translated doc plus
3270
+ an untranslated parser meant a **blind and green** gate, which is the silent way to fail.
3271
+ Here, a translated hook plus an untranslated test meant a **red** test, which is the noisy
3272
+ way. The difference between the two cases is only whether a test points at it.
3273
+
3274
+ ### Green measured
3275
+
3276
+ `hooks/test/run.sh` **84 ok / 0 fail** · `bash -n` clean on the 12 scripts · zero PT above
3277
+ the threshold in the 16 files · **live** proof (not a fixture): `pre-edit` matches real
3278
+ gotchas by the `**Trigger:**` field from rc.2, and `session-start` injects the digest in
3279
+ English.
3280
+
3281
+ ## [3.6.0-rc.4] — 2026-08-23
3282
+
3283
+ ### Added
3284
+
3285
+ - **The 23 `SKILL.md` files in English STE** (batch B5). This is the surface that
3286
+ matters most for routing: the `description` and `when_to_use` of the frontmatter are
3287
+ the text that decides **which skill activates**. `skills-lint.sh` 38 ok / 0 fail — the
3288
+ listing budget did not grow, and it is unidirectional (shrink passes, grow fails).
3289
+
3290
+ ### Fixed
3291
+
3292
+ - **`skills/promote/SKILL.md` gained the word "another"** where the Portuguese source
3293
+ does not have it. *"Desambiguação — critério de quem já ativou"* became *"once ANOTHER
3294
+ skill has already activated"*, which changes the purpose of the section: it talks
3295
+ about **itself** already activated, not about a sibling skill. A single invented word,
3296
+ with a semantic effect.
3297
+ - **`skills/remediate/SKILL.md` lost the rationale** for why the disambiguation lives in
3298
+ the body and not in the resident listing — the reason is resident context cost.
3299
+
3300
+ ### The batch stopped midway, and the design held
3301
+
3302
+ B5 hit the session limit with **23 of 27 agents dead**. Nothing got lost, for two
3303
+ reasons worth keeping as a harness lesson:
3304
+
3305
+ 1. **The agent writes the file before it reports.** The 17 that had already run were on
3306
+ disk. What got lost was their verification, not their work.
3307
+ 2. **Resume worked by MEASURED DENSITY, not by `git status`.** `remediate/SKILL.md` was
3308
+ at `1.6` — half translated. By `git status` it counted as "done" and would have
3309
+ stayed silently bilingual. The count caught it.
3310
+
3311
+ ## [3.6.0-rc.3] — 2026-08-23
3312
+
3313
+ ### Added
3314
+
3315
+ - **`interview/`, `guardrails/`, the 17 `modules/` patches, `PRINCIPLES.md`, and the two
3316
+ READMEs in English STE** (28 files, batch B3).
3317
+
3318
+ ### Fixed during the rewrite
3319
+
3320
+ - **`PRINCIPLES.md` got an INVENTED sentence that contradicted itself.** The English
3321
+ stated *"a cheap re-runnable signal allows one retry"* — a claim the Portuguese does
3322
+ not have anywhere — and the **next** sentence of the same paragraph said *"the cap is
3323
+ 2 retries"*. An internal contradiction in a normative doc. The **invention** checker,
3324
+ added after B1, caught it. Without it, this would have passed.
3325
+ - **`PRINCIPLES.md § 1` lost the criterion that justifies the file itself:** *"ou outro
3326
+ agente sem minha memória"*. The English collapsed this to "someone else" and dropped
3327
+ the case of an agent with no memory, which is the reason `/docs/` exists. The two
3328
+ concrete examples (`sem emojis`, `PRs pequenas vs bundles grandes`) also disappeared,
3329
+ and in § 8, the cap on **questions per call** (≤4) was lost, leaving only the item cap.
3330
+ - **`guardrails/dsl-com-ordem-obrigatoria.md` had its THESIS inverted.** The Portuguese
3331
+ says a mandatory-order builder *"que só falha em runtime"* is a deferred bug — the
3332
+ condition is the point, because the whole doc argues that **type-state makes
3333
+ mandatory order safe**. The English stated unconditionally that the builder *is* a
3334
+ bug, the opposite of what the file argues.
3335
+ - **`modules/prisma-postgres/ach.patch.md` lost the outer ```` ```md ```` fence** of
3336
+ § 6. Patches use it to mark the **literal insertable** content. Without it, the
3337
+ heading became a live heading of the patch itself. 10 markers in the original,
3338
+ 8 in the English.
3339
+ - **`ops/rule.md` lost "Insistência forte aqui"** — a behavioral emphasis directive,
3340
+ not decoration: the user pushes to call something a hard rule when it has no trigger.
3341
+ - **`ops/deliver.md` lost 4 of the 6 ephemeral-override phrases.** Restored **in
3342
+ Portuguese on purpose** — `vai`, `pode ir`, `direto`, `não precisa revisar` are
3343
+ **user utterances**, not artifact text, and the conversation follows the language
3344
+ the user speaks. The English equivalents were added alongside.
3345
+ - **`ops/export-template.md` diluted "verde e commitado" into "clean, committed"**,
3346
+ losing the AWM criterion of **passing tests** and duplicating the `git status` check
3347
+ from the next sentence.
3348
+
3349
+ ### Changed
3350
+
3351
+ - **The rest of the guardrail frontmatter is now in English:** `titulo:`/`sinal:`/
3352
+ `parametros:`/`fonte:` → `title:`/`signal:`/`params:`/`source:`, with the two `awk`
3353
+ calls in `afm-pre-edit.sh` (lines 101 and 109) in the same commit. Completes the
3354
+ rename from rc.2.
3355
+
3356
+ ### New gotcha
3357
+
3358
+ - **`ste-lint` lints inside a NESTED fence again.** The emitter treats a fence as a
3359
+ toggle, so ```` ```ts ```` inside ```` ```md ```` flips the parity and the patch's
3360
+ literal text becomes `S2`/`S3`. A pure false positive, and a false positive is how a
3361
+ gate gets turned off. Recorded in `docs/gotchas.md` with the fix cost stated.
3362
+
3363
+ ## [3.6.0-rc.2] — 2026-08-23
3364
+
3365
+ > **The literal markers are English now too.** They were the MOST parsed text in
3366
+ > the repo and the last one in Portuguese. Atomic rename: 121 files, docs and parsers in the same
3367
+ > commit.
3368
+
3369
+ ### Changed (BREAKING for a materialized instance)
3370
+
3371
+ | before | now | who parses it |
3372
+ | --- | --- | --- |
3373
+ | `**Gatilho:**` | `**Trigger:**` | `afm-pre-edit.sh` (gotchas + learnings) |
3374
+ | `*Verificação:*` | `*Verification:*` | `guardrails.sh --audit` |
3375
+ | `[A DEFINIR]` | `[TBD]` | `afm-session-start.sh`, `afm-health.sh`, `PRINCIPLES.md` #4 |
3376
+ | `## o que foi pedido` | `## what was asked` | `afm-session-end.sh` **prints it literally** |
3377
+ | `## como foi executado` | `## how it was executed` | same |
3378
+ | `## O que refutaria isto` | `## What would refute this` | guardrail template (mandatory) |
3379
+ | `não-coberto:` | `not-covered:` | `diagnose` output tag |
3380
+ | frontmatter `origem:`/`superficie:`/`gatilho:`/`custo:` | `origin:`/`surface:`/`trigger:`/`cost:` | `afm-pre-edit.sh` (`awk /^trigger:/`) |
3381
+ | `falha-recorrente` / `regra-dura` | `recurring-failure` / `hard-rule` | `origin:` enum values |
3382
+
3383
+ `[A DEFINIR]` became **`[TBD]`**, not `[NEEDS CLARIFICATION:]`. The two markers have
3384
+ distinct semantics, and `feature-spec.template.md` documents this: `[TBD]` is a decision the
3385
+ owner deferred on purpose, `[NEEDS CLARIFICATION:]` is a gap that blocks execution.
3386
+ Merging them would erase the distinction that `afm-health.sh` counts separately.
3387
+
3388
+ ### The silent break the rename almost caused
3389
+
3390
+ Renaming the doc without renaming the parser leaves the hook **blind, and green**. It happened here:
3391
+ `docs/gotchas.md` moved to 25 `**Trigger:**` and 0 `**Gatilho:**` while
3392
+ `afm-pre-edit.sh` still ran `grep -iEn "gatilho.*"`. The hook would inject zero gotchas and
3393
+ no test would fail. Worse: two asserts in `hooks/test/run.sh` check for the **absence**
3394
+ of the string `Verificação` — with the doc translated, they would pass by vacuity.
3395
+
3396
+ This is why the rename is one commit. A doc and its parser out of sync is how supervision dies in
3397
+ silence.
3398
+
3399
+ ### Green, measured
3400
+
3401
+ `hooks/test/run.sh` 84 ok / 0 fail · `guardrails.sh --audit` 9 rules ↔ 9 runners ·
3402
+ `--self-test` 11 ok / 0 fail · `update-rehearsal.sh` 13 ok / 0 fail (with `[TBD]`
3403
+ preserved untouched) · `ste-batch-check.sh` over `ops/` rc=0 · `ste-lint --scope strict`
3404
+ 0 block in `ops/`.
3405
+
3406
+ ### Added
3407
+
3408
+ - **All of `ops/` in English STE** (21 files, batch B2). The adversarial
3409
+ checker found 5 CRITICAL, **all** of the same class: a parsed string literal
3410
+ translated as if it were prose. No invention, no Portuguese left.
3411
+
3412
+ ## [3.6.0-rc.1] — 2026-08-23
3413
+
3414
+ > **The toolkit becomes english-first.** Every artifact the AFM produces is born in English under
3415
+ > ASD-STE100. The chat conversation follows the language the user speaks — a reply is not an
3416
+ > artifact, no one reads it later.
3417
+
3418
+ ### Changed
3419
+
3420
+ - **The whole `core/` was rewritten in English STE** (37 files: `afm.md`, `protocol.md`,
3421
+ `ach.md`, `prd.md`, `ust.md`, `gotchas.md`, 12 rubrics, 19 templates). Each file went
3422
+ through a rewrite and an **independent adversarial check** — a second
3423
+ agent reads the Portuguese original and the English result and reports what got lost.
3424
+
3425
+ ### Measured, and smaller than promised
3426
+
3427
+ - **Tokens: 41,400 → 40,050 (−3.3%)**, 29 of the 37 files shrank. The ones that shrank
3428
+ most are the hot ones: `afm.md` −322, `protocol.md` −191. In WORDS the text
3429
+ **grew 9%** — STE forbids the semicolon and caps a sentence at 20-25 words, so
3430
+ one dense Portuguese sentence becomes two or three English ones, each repeating the subject.
3431
+ - **The original estimate was −37% and it was wrong by an order of magnitude.** This stays
3432
+ on record because the estimate drove a scope decision. The real gain from
3433
+ english-first is not compression: it is unambiguous text for a parser, more reach
3434
+ in distribution, and reasoning in the language most of the code was written in.
3435
+
3436
+ ### Fixed during the rewrite itself
3437
+
3438
+ - **`rubrics/when-to-create-dsl.md` had its CRITERION changed by the translation.** The
3439
+ Portuguese said `≥ 3 call-sites repetem a mesma sequência de ≥ N chamadas primitivas`,
3440
+ with **N deliberately unspecified**. The first rewrite collapsed the two
3441
+ thresholds into 3. This is the class of damage that no count catches, only adversarial
3442
+ verification does. Restored.
3443
+ - **32 occurrences of `✅`/`❌` became `YES`/`NO`** in 5 rubrics, despite an
3444
+ explicit instruction to preserve emoji. Restored.
3445
+ - **One sentence was INVENTED** in `when-to-create-lib.md` — a rationale the original did not
3446
+ have. The first round's verifier hunted only for loss, not invention, so it did not
3447
+ catch it. The prompt now reports `loss | invention | inversion`, with invention
3448
+ weighted the same: the file is the source of truth, and an invented clause becomes doctrine.
3449
+
3450
+ ## [3.5.0-rc.14] — 2026-08-22
3451
+
3452
+ > **A repository cannot declare its own hooks** — if it could, `git clone` would be remote code
3453
+ > execution. The single opt-in step is irreducible in git. What did not need to be irreducible
3454
+ > was the operator **typing** the command by hand, with a silent chance to forget it.
3455
+
3456
+ ### Added
3457
+
3458
+ - **`ops/rule.md` step 7 becomes a 4-tier surface escalation** — tier 1: the manager the project
3459
+ **already** has (husky/lefthook/pre-commit); tier 2: propose husky or lefthook for a Node
3460
+ project; tier 3: `core.hooksPath` through the aggregator's own `--install`; tier 4: CI. The
3461
+ operator can ask for another manager — the table is a recommendation, not a rail. **Running the
3462
+ install is allowed. Self-installing is not** — an opt-in that installs itself is not an opt-in.
3463
+ - **`hooks/afm-pre-commit.sh` (`PreToolUse`, matcher `Bash`)** — the **tier 3** gate, for a
3464
+ project with no native surface at all. It blocks `git commit` with `permissionDecision:
3465
+ "deny"` when the consumer's aggregator exits 1. **Second carve-out** of the hooks golden rule
3466
+ (ADR 0015, amendment to 0013), by the **same** criterion: harm asymmetry. Blocking a commit
3467
+ delays; blocking an `Edit` disables the session — and the `Edit|Write` matcher stays forbidden.
3468
+ - **`templates/guardrail-runner.template.md`** gets the activation step with the tiers.
3469
+
3470
+ ### Changed
3471
+
3472
+ - **The whitelist of blocking hooks has two names and stays a whitelist.** The smoke test assert
3473
+ moved from "only Stop" to "only Stop and pre-commit", and it fails a third one — it also fails
3474
+ if the blocking hook is wired on the `Edit|Write` matcher. **9 new asserts** cover the tiers,
3475
+ `--no-verify`, the prose mention, exit 2, and the no-op outside an AFM project (74 → 84).
3476
+ - **`hooks/README.md`** and **`ach.md § 3.1`** updated: the `hook` component-type decides in
3477
+ **two** named cases.
3478
+
3479
+ ### Notes
3480
+
3481
+ The aggregator's `--install` stopped just **printing** the command and now executes it: running
3482
+ something named `--install` is the authorization. It refuses to overwrite another manager's
3483
+ `core.hooksPath` — silently disarming someone else's gate is the failure mode this mechanism
3484
+ fights.
3485
+
3486
+ ## [3.5.0-rc.13] — 2026-08-22
3487
+
3488
+ > **The hard rule was already executable — and no one ran it.** Eight rules, eight 0/1 triggers
3489
+ > written in prose inside the doc, zero automated: no CI, no git hook, no husky. A trigger
3490
+ > that depends on someone remembering fails exactly when context gets tight, and that is how the
3491
+ > marketplace cache fell ten releases behind. This release closes the *rule → gate* path.
3492
+
3493
+ ### Added
3494
+
3495
+ - **`ops/rule.md` § Instalar o portão** — canonical procedure for "a hard rule with a 0/1
3496
+ trigger becomes a check that fires on its own". Seven steps, in the order that matters:
3497
+ find the surface the project **already runs** (a new tool is a last resort, with declared
3498
+ cost) → write the runner → **measure HEAD before you switch it on** → measure the cost →
3499
+ prove doc↔runner sync → register the guardrail → propose activation to the operator. No new
3500
+ op and no new skill: installing the gate continues the op that creates the rule. It is not a
3501
+ separate operation.
3502
+ - **Step 3.7 in `ops/rule.md`** — the surface (`pre-commit` | `ci` | `agente`) is chosen
3503
+ **before** the rule is written, and the line becomes `*Verification (<superficie>):*` citing
3504
+ the runner. `agente` is honest for a heuristic trigger, not for a precise trigger no one
3505
+ automated.
3506
+ - **`templates/guardrail-runner.template.md`** — the runner contract (exit **0/1/2**), the
3507
+ skeleton, and the "before you call it done" checklist. `framework-template/` stays
3508
+ markdown-only: the runner travels as a fenced block, because the only distributed shell is in
3509
+ `hooks/`, which has its own smoke test.
3510
+ - **`docs/guardrails/` gains `origin:`** — `recurring-failure` (from the CRITIC loop) and
3511
+ `hard-rule` (from a rule already written that no one ran). **One family, two entry doors**:
3512
+ the artifact is the same object — a 0/1 check plus a burden of refutation — and describing the
3513
+ same thing in two vocabularies is the drift rule 34 exists to stop.
3514
+
3515
+ ### Changed
3516
+
3517
+ - **`rubrics/failure-classification.md`** and **`ops/remediate.md`** now **compose** the
3518
+ install procedure instead of describing it: the *mechanizable* destination points to
3519
+ `rule.md § Instalar o portão` (rule 34 — a procedure lives in one place only).
3520
+ - **`templates/guardrail.template.md`** gains frontmatter (`id`/`origem`/`superficie`/
3521
+ `gatilho`/`custo`) and the two-origin table.
3522
+
3523
+ ### Notas do dogfooding (este repo como primeira instância)
3524
+
3525
+ The mechanism was built here before it was described: `test/guardrails.sh` + 9 runners +
3526
+ ratchet + a versioned `pre-commit`. Three lessons became normative text above:
3527
+
3528
+ 1. **A count rule measured over the index is a silent false negative.** With a budget of 7, a
3529
+ commit that adds 1 new violation counts 1 ≤ 7 and passes clean. A *diff* rule measures the
3530
+ index with a budget of 0; a *count* rule measures the whole repo, always.
3531
+ 2. **Exit 2 ("could not verify") must be distinct from 1 ("violated").** Rule 21 depends on
3532
+ `grep -P`; the first machine without GNU grep would turn the gate into a permanent block.
3533
+ 3. **A gate that fails half the repo measures a bad threshold, not drift.** The "inline
3534
+ numbered procedure" check flagged 13 of 23 `SKILL.md` files — including legitimate
3535
+ `init`/`refactor` interviews. It was **removed**, not loosened. And the threshold belonged to
3536
+ the agent, when the constant is the operator's (3.6).
3537
+
3538
+ The gate, on its first real use, rejected the commit that installed it — for violating rule 30.
3539
+
3540
+ ## [3.5.0-rc.12] — 2026-08-21
3541
+
3542
+ > **The session that committed everything looked the same as the session that did nothing.** The
3543
+ > rc.11 gate failed exactly in the disciplined case, and it failed silently. This is the tenth
3544
+ > occurrence of the same family in this line of work — this time inside the mechanism built to
3545
+ > stop it.
3546
+
3547
+ ### Fixed
3548
+
3549
+ - **The "session with work" signal ignored committed work.** `afm-session-end.sh` measured work
3550
+ only by `git diff --name-only HEAD` — **uncommitted** change. A session that delivered a lot and
3551
+ committed everything leaves a clean tree, and the hook exited with exit 0 before any evaluation:
3552
+ no handoff, no reflection charge. Measured in the plugin repo right after the rc.11
3553
+ commit: **clean tree, 23 events from this session in the ledger**. Now the signal is
3554
+ disjunctive — uncommitted diff **or** `s=<sid6>` events in the append-only ledger, which
3555
+ `afm-capture.sh` already writes on every commit. `files_touched` does not invent a list when
3556
+ everything was committed. It says so and points to `summary`.
3557
+ - **Two defects in the gate's own fixture**, both in the class "the assert does not prove what it
3558
+ claims": the fixture appended events **after** the commit (dirtying the tree that the
3559
+ precondition assert required clean), and it wrote `s=` in the wrong field — the real ledger
3560
+ format is `<kind> | s=<sid6> | <título>`, with the sid in the middle. With the field swapped,
3561
+ the hook's `grep` did not match, and the assert failed for the wrong reason.
3562
+
3563
+ ### Added
3564
+
3565
+ - **`$schema` in `hooks/hooks.json`** for IDE typing. No published schema exists for a plugin's
3566
+ `hooks.json` — SchemaStore has five Claude Code schemas (`settings`, `plugin-manifest`,
3567
+ `marketplace`, `keybindings`, `launch`) and none matches this file by `fileMatch`. It points to
3568
+ the `settings.json` one, which defines the `hooks` block in full (31 events, `$defs/hookMatcher`,
3569
+ `hookCommand`) and has `additionalProperties: true` at the top. The trade-off is recorded in
3570
+ `_schema_note` inside the file itself. `plugin.json` and `marketplace.json` are already typed
3571
+ automatically by `fileMatch` — they do not need the key.
3572
+ - **Regression assert** in the smoke test: a session with **committed** work still blocks, with an
3573
+ explicit clean-tree precondition (otherwise the next assert proves nothing).
3574
+
3575
+ ### Consumer migration
3576
+ None. `CONSUMER_VERSION` stays at `3.5.0-rc.11` — the fix is in the hook, which travels with the
3577
+ plugin, and the `$schema` key is IDE tooling.
3578
+
3579
+ ## [3.5.0-rc.11] — 2026-08-21
3580
+
3581
+ > **`reflect` depended on the model remembering to call it — and it did not remember.** The operator ran
3582
+ > several protocols with not one reflection. This release swaps the invitation for a gate: `Stop`
3583
+ > now enforces it, cadence moves from the script to the project artifact, and a recurring failure
3584
+ > that a command would catch stops turning into a document.
3585
+
3586
+ ### Added
3587
+
3588
+ - **`docs/guardrails/` — the fourth failure destination, with precedence.** `rubrics/failure-classification.md`
3589
+ had three destinations (`procedure`, `learning`, `transiente`), and the first two are **documents
3590
+ that inform a reader**: they depend on someone remembering to open the runbook or having read the
3591
+ lesson. A **mechanizable** recurring failure — a boundary break between modules, a non-exhaustive
3592
+ union, a mandatory order in a DSL — now has its own destination: a 0/1 check that fires on its own.
3593
+ The precedence is explicit: when a failure fits both, it is a guardrail. New
3594
+ `templates/guardrail.template.md` with the mandatory `## What would refute this` section,
3595
+ which flips the burden onto whoever wants to loosen the gate later.
3596
+ - **Mandatory surface discovery before proposing a guardrail** (`ops/remediate.md` step 3.5).
3597
+ Reads what the project **already runs** — linter, type-check, CI, git hooks, versioned scripts —
3598
+ and extends that. A new tool is a last resort, explicit and with a stated cost: a gate that
3599
+ requires adopting one more tool tends to cost more than the error it catches.
3600
+ - **`ops/reflect.md § 1.2` — the second sweep.** `remediate` consumes `.afm-log-failures/`, which
3601
+ logs a failure only with a **terminal external signal**. The error the agent caught while reading
3602
+ its own diff never reaches there — it was fixed before it became a signal. It is the costliest
3603
+ class, because its cost is context, invisible in the ledger. `reflect` is the only point in the
3604
+ cycle that sees it.
3605
+ - **`ops/evolve.md` Phase 1.3.5 — a guardrail axis in the cycle.** Cross-checks `reflect`'s
3606
+ candidates against the mechanizable failures in the substrate and sees the **pattern across
3607
+ failures**: three different defects that share the same missing surface are a finding that none
3608
+ of them alone reveals.
3609
+ - **`REFLECT_EVERY` / `EVOLVE_EVERY` / `REFLECT_TASKS`** in `core/protocol.md § 2`. The `cadence`
3610
+ field was declared in § 1 with **no parameter line at all** — a field without a parameter.
3611
+ Declarable per project on the `cadence:` line of `docs/_focus.md`.
3612
+ - **`hooks/lib/afm-cadence.sh`** — single source (rule 34) for the count and for reading cadence,
3613
+ now that there are two readers: `afm-health.sh` reports on `SessionStart`, `afm-session-end.sh`
3614
+ enforces on `Stop`.
3615
+
3616
+ ### Changed
3617
+
3618
+ - **NAMED CARVE-OUT FROM THE HOOKS GOLDEN RULE (ADR 0013).** `Stop` can return
3619
+ `decision:"block"`, and **only it can**, and **only** for reflection cadence. The other six hooks
3620
+ stay forbidden — the original reason still holds: a hook that blocks an `Edit` turns a hook bug
3621
+ into an unusable session. `Stop` is different because blocking there does not stop work, it
3622
+ delays closing; **the damage asymmetry is the whole argument**. Three independent guards:
3623
+ `stop_hook_active` (loop), a cap of 1 block per session, and the `cadence: reflect=off` opt-out.
3624
+ A new smoke-test gate fails if any other hook copies the pattern.
3625
+ - **The unblock is 0/1 on an artifact, never on the agent's word** — a non-empty `reflection:`
3626
+ in the session file. A block that clears with a mere statement is not a block.
3627
+ - **"Skip this one" is logged debt, not disappearance.** `reflection: skipped — <reason>` + `debt: true`
3628
+ in the frontmatter; `afm-health.sh` counts and reports it, `evolve` treats it as evidence. Without
3629
+ the record, skipping would be indistinguishable from never happening.
3630
+ - **P3 uses native Plan Mode as a mechanism, not as an analogy** (`templates/protocols/p3.md`):
3631
+ `EnterPlanMode` in phase 1, `ExitPlanMode` at the transition. Degradation is declared — without
3632
+ the mode, it falls back to summary + `AskUserQuestion`, and **the gate criterion is the same in
3633
+ both**.
3634
+ - **`ops/deliver.md`** drops the outdated premise *"a skill does not force the session into plan
3635
+ mode"* — true when written, false today.
3636
+ - **`afm-session-end.sh` locates the session file by a `sid` glob**, not by a recomputed name.
3637
+ The dedup flag in `$TMPDIR` was doing `exit 0` on the second `Stop`, killing the block evaluation
3638
+ exactly where the re-check matters.
3639
+
3640
+ ### Fixed
3641
+
3642
+ - **The P3 transition gate was vacuously red.** `grep '[NEEDS CLARIFICATION:]'` fails a plan that
3643
+ only **mentions** the marker inside a code span — describing the gate itself. Measured in
3644
+ delivery 001: the only hit was the line documenting the gate. The command now strips code spans
3645
+ before searching (`sed 's/`[^`]*`//g'`). A gate that fails for talking about itself is as useless
3646
+ as a gate that passes without looking.
3647
+ - **Cadence anchored only on `deliverable` would be a dead trigger.** The event fires only when a
3648
+ `docs/features/*/tasks.md` zeroes out its `- [ ]`; work that never opens a feature folder never
3649
+ emits it. Measured on the plugin repo: **25 `task` events, zero `deliverable`** over two months.
3650
+ The block condition is disjunctive now. A gate that never fires is indistinguishable from one
3651
+ that works — caught before shipping, in the fixture.
3652
+
3653
+ ### Consumer migration
3654
+ 1. **`docs/_focus.md`** can gain the line `cadence: reflect=1 evolve=3` (optional — if absent,
3655
+ the defaults apply). `reflect=off` turns off the `Stop` block.
3656
+ 2. **`docs/guardrails/`** is born on demand, like `procedures/` and `learnings/`. Nothing to create
3657
+ by hand.
3658
+ 3. **New visible behavior:** at the end of a session with work done and cadence reached, closing
3659
+ is blocked **once** with an instruction to run `/afm:reflect` or log the debt. Anyone who does
3660
+ not want this declares `cadence: reflect=off`.
3661
+
3662
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.5.0-rc.8` → `3.5.0-rc.11`.
3663
+
3664
+ ## [3.5.0-rc.10] — 2026-08-20
3665
+
3666
+ > **The guardrail suggestion was inert, and the eval proved it.** rc.6 put the trigger in a file
3667
+ > nobody reads during common work. This release moves it to where it bites.
3668
+
3669
+ ### Fixed
3670
+
3671
+ - **The guardrail now proposes itself at `PreToolUse`, at write time.** rc.6 put the signal in
3672
+ `interview/triggers.md`, and the `guardrail-sinal-casa` eval failed **0/5**: the transcript shows
3673
+ the agent doing the right task, **noticing** the exhaustiveness gap, and dismissing it as "out of
3674
+ scope". `SessionStart` never mentioned the catalog, and the agent does not read `triggers.md`
3675
+ during code work. `PreToolUse` is already the mechanism that **provably** fires — it is what
3676
+ delivers gotcha, learning, and rule by path-match. The block runs the `sinal` of the catalog
3677
+ entries, skips the ones already installed, and proposes **once per session** (never per file),
3678
+ only on code edits — proposing a code guardrail when editing a `.md` file is noise. Measured:
3679
+ **34 ms** per edit.
3680
+ - **The catalog signals matched inside `node_modules`.** `dsl-com-ordem-obrigatoria` was proposed
3681
+ because the `ajv` dependency has `return this` — **30 hits**, none in the project code. It only
3682
+ showed up because the hook runs in a clean subshell, **without the shell aliases of the person
3683
+ who wrote the catalog**: validating the signal on the command line gave "no match", and the hook
3684
+ gave "match". A new gate requires `--exclude-dir=node_modules` on every recursive signal.
3685
+ - **The `SessionStart` signal enumeration was stale, and got removed, not updated.** The line
3686
+ listed **5** signals against **24** in `triggers.md` — a second source of truth, out of sync
3687
+ since before this session. Enumerating the 24 would cost 1,050 chars per session, and the eval
3688
+ had already shown that enumerating **does not** make the agent propose anything. Listing 5 of 24
3689
+ did not inform: it misinformed. The line now points to the file and says where the guardrail
3690
+ proposal actually arrives. It got **31 chars shorter**.
3691
+
3692
+ ### Verification
3693
+
3694
+ Hook: fires on a code edit, stays silent on the second edit of the same session, stays silent on
3695
+ `.md`. Smoke 62 → **63** asserts. The end-to-end eval was triggered but **has not returned yet**
3696
+ as this commit is written — what is proven here is the hook's behavior, not the change in the
3697
+ agent's conduct. If the eval keeps failing, the diagnosis is wrong and the finding goes back to
3698
+ the table.
3699
+
3700
+ ### Consumer migration
3701
+ - **Consumer-noop.** `hooks/` and `guardrails/` are read from the plugin, not materialized.
3702
+ `CONSUMER_VERSION` stays at `3.5.0-rc.8`.
3703
+
3704
+ ## [3.5.0-rc.9] — 2026-08-20
3705
+
3706
+ > **The MCP stops charging users who never use it, and now says what to do when it fails.**
3707
+ > Reverts, with a better design, two refusals recorded in rc.2.
3708
+
3709
+ ### Added
3710
+
3711
+ - **Lazy bootstrap.** `mcp/bootstrap.mjs` had two paths and now gets both right: **already
3712
+ built** imports `dist/server.js` and gets out of the way (the common case, SDK server as
3713
+ before); **not built** starts a **dependency-free** JSON-RPC stub that answers `initialize` and
3714
+ `tools/list` right away, and installs+builds only on the **first real `tools/call`**. A user who
3715
+ installed the plugin and never calls `web_read` pays no `npm install` at all.
3716
+ - **`tool.json` as the single source for the tool definition**, read by the SDK server and by the
3717
+ stub. Duplicating it would make the stub announce a different tool than the one that answers —
3718
+ drift invisible until someone calls it with an argument only one side knows. `src/handler.ts`
3719
+ does the same for the logic: one pure handler, callable by both, with neither reimplementing
3720
+ the other.
3721
+ - **Actionable error for non-fetchable input.** `file:///x`, `/local/path`, `./note.md`, and
3722
+ exotic schemes now say **what to do next** ("read a local file with the Read tool"). Before,
3723
+ `file://` reached the agent as `fetch failed (error): fetch failed` — handled, but silent.
3724
+ **This is not a security control, and the code comment says so**: it does not block a private
3725
+ IP and does not try to; destination validation stays refused (rc.2), with the bypass
3726
+ documented.
3727
+
3728
+ ### Fixed
3729
+
3730
+ - **The bootstrap failure was a stack trace on stderr that never reached the model.** Now it is a
3731
+ sentence that comes back as a **tool result** — the agent reads it, knows it is not its own
3732
+ request's fault, knows a retry is pointless, and gets the fallback (`WebFetch` + the mark in
3733
+ `research.md`). **This goes further than the gap report proposed:** the original proposal was
3734
+ `console.error` + `exit(1)`, which goes to the same stderr that was already inherited and just
3735
+ as invisible — that is why rc.2 refused it. The right channel was not the message, it was its
3736
+ **destination**.
3737
+
3738
+ ### Why rc.2 refused this and rc.9 does it
3739
+
3740
+ rc.2 refused the `docs/.afm-version` gate in the bootstrap because the proposed form (`exit(0)`
3741
+ with no handshake) makes the client mark the server as `failed` — visible noise traded for silent
3742
+ install. The argument still holds: **this release does not implement that gate.** The stub solves
3743
+ the same problem a different way — it answers the handshake and defers the cost, so nobody sees
3744
+ anything until real use. Measured: the install `cwd` is the plugin directory, so the cost avoided
3745
+ is one install per plugin version, not per project.
3746
+
3747
+ ### Verification
3748
+
3749
+ The stub is hand-written protocol, so it was tested **against the real client**, not only against
3750
+ asserts: with `dist/` absent, a `claude -p` session lists
3751
+ `mcp__plugin_afm_research__web_read` and **no build fires**. Package tests: 17 →
3752
+ **23**. One of them covers the drift that `tool.json` created — the default `1500` lives in the
3753
+ JSON description and in `DEFAULT_BUDGET_TOKENS`, because JSON does not interpolate, so a gate
3754
+ compares the two.
3755
+
3756
+ ### Consumer migration
3757
+ - **Consumer-noop.** `mcp/` is install-side surface, not materialized.
3758
+ `CONSUMER_VERSION` stays at `3.5.0-rc.8`.
3759
+
3760
+ ## [3.5.0-rc.8] — 2026-08-20
3761
+
3762
+ > **The first rule the operator does not turn off — and the honest statement of how far that goes.**
3763
+
3764
+ ### Added
3765
+
3766
+ - **Hard rule 21 — `INALIENÁVEL`: code and tests in English.** Identifier, function name,
3767
+ test name, and comment in English, in any language and any project. **A string facing the end
3768
+ user follows the product language** — the rule is about the code, not about content the user reads.
3769
+ *Verification:* `grep -rqP` over declaration lines for a non-ASCII character. The command proves
3770
+ **half**: it catches `criarPedidoComEndereço` and `it("devolve o pedido válido")`. **It does not
3771
+ catch** (a) Portuguese without an accent (`criarPedido`) — judgment call, § 5.1; (b) **a comment,
3772
+ on purpose** — English prose uses an em dash, curly quotes, and an ellipsis, so non-ASCII in a
3773
+ comment is noise, not signal. Measured: a correct English comment flagged a violation only because
3774
+ it had a `—`. Stating what the command **does not** prove is what separates an honest partial
3775
+ trigger from an empty promise.
3776
+
3777
+ The `\b` in the alternation is **load-bearing**: without it, `it` matches inside "delimitam" and
3778
+ `type` matches inside "prototype". The first version of the trigger found real violations **for the
3779
+ wrong reason** in this same repo — finding the right answer by accident is worse than not finding
3780
+ it, because the next case will not have the accident.
3781
+ - **The concept of an `INALIENÁVEL` rule, with three mechanical teeth and a stated limit.**
3782
+ The interview **does not offer it** in the Step 5 opt-out (and does not omit it silently either: it
3783
+ lists it with a note that it is not an opt-out — offering it and then refusing would be worse). The
3784
+ `ops/rule.md` **refuses** to remove it or mark it N/A, even under `SUPPRESS_SUBGATES`. The
3785
+ `ops/diagnose.md` gains axis **A5b**: an inalienable rule missing from the instance is **CRITICAL**.
3786
+
3787
+ **The limit, stated in full:** `docs/afm.md` is a file in the operator's repo, and nothing stops
3788
+ the operator from editing it. AFM **detects** the removal, it does not **prevent** it. Calling this
3789
+ "impossible to change" would be exactly the empty promise that `protocol.md § 6` carried until
3790
+ rc.2 — and the value of having fixed that disappears the moment another one like it gets written.
3791
+
3792
+ **Inalienable is not retroactive.** In a brownfield project it enters forward-only (§ 3.1) like any
3793
+ other rule: requiring retroactivity would make adoption impossible in any codebase not born under it.
3794
+
3795
+ ### Fixed
3796
+
3797
+ - **`src` was hardcoded in rule 6.** It became `{{src-path}}`, now with **two** callers (rules 6 and
3798
+ 21) — introducing a placeholder used only once would repeat the smell of the orphan `{{path}}` that
3799
+ rc.5 fixed.
3800
+
3801
+ ### Consumer migration
3802
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.5.0-rc.5` → `3.5.0-rc.8`.
3803
+
3804
+ **Copy** (the rule is new; there is no From→To):
3805
+
3806
+ | Consumer file | Action |
3807
+ | --- | --- |
3808
+ | `docs/afm.md` § 3 | add rule **21** verbatim from `core/afm.md`, filling in `{{src-path}}` |
3809
+
3810
+ 0/1 detection for an instance that does not have it yet (this is the `diagnose` axis A5b):
3811
+
3812
+ ```sh
3813
+ grep -c 'INALIENÁVEL' docs/afm.md # 0 → needs materializing
3814
+ ```
3815
+
3816
+ > An instance whose § 3 was renumbered locally: 21 enters under the **number the plugin uses**, not
3817
+ > the next free one. Inalienable is citable by number across projects, and renumbering it would break
3818
+ > the citation.
3819
+
3820
+ ## [3.5.0-rc.7] — 2026-08-20
3821
+
3822
+ > **The harness gets a judge, and a canary that distrusts it.** Phase 5 of the active-executor
3823
+ > plan: measure whether the improvements work, without the measurement itself turning into theater.
3824
+
3825
+ ### Added
3826
+
3827
+ - **`llm` grader in the runner** (`test/lib/eval-run.py`), on the **official schema** and only
3828
+ there: `criteria` (required) + `focus` (`last_message` · `trace` · `files` · `{source: file,
3829
+ path}`), verdict by **2-of-3 majority**, model via `--judge-model` (default `haiku`). What is
3830
+ **not** publicly documented — a numeric threshold, a model field in `case.yaml`, a return format,
3831
+ `baseline` — was left **out on purpose**: a schema invented today is a case that does not run
3832
+ tomorrow, and no one finds out until they try.
3833
+ - **Skipped is not passed.** Without `--judge` the `llm` grader returns `None` and drops out of the
3834
+ **denominator** of the score. Counting it as a failure would punish someone who did not pay for the
3835
+ API. Counting it as a pass would inflate the score with coverage that does not exist — the same
3836
+ denominator bug that already cost this repo dearly, when `listing_total_all_plugins` summed three
3837
+ plugins and called itself "all". The self-test now locks in exactly this distinction.
3838
+ - **Judge canary** (`test/judge-canary.sh`) — calibration in **two directions**, which
3839
+ `guardrails/agentes/14-medicao-gameavel.md` requires before you trust a gate judged by an LLM:
3840
+ - **impossible passed** → the judge rubber-stamps, and every `llm` score is inflated;
3841
+ - **trivial failed** → the judge rejects everything, and every score is deflated.
3842
+ A judge that gets both wrong is noise; one that gets **one** wrong is bias with a known direction,
3843
+ which is worse, because it looks like it works. **It found a bug on the first run** — in the
3844
+ harness, not the judge: the evidence was passed outside the transcript, the place
3845
+ `_target_text` reads from, and the judge was judging a void in both canaries. One canary alone
3846
+ (the impossible one) would have passed green with a blind judge, because a blind judge fails
3847
+ everything.
3848
+ - **Two eval cases for the guardrail suggestion** (phase 3): `guardrail-sinal-casa` (a union of
3849
+ literals with an incomplete `switch` — the signal fits, and the agent proposes **without claiming
3850
+ it installed** anything) and `guardrail-negativo` (a pure-content project — no signal fits, and
3851
+ proposing there would be the noise that makes the operator stop reading proposals).
3852
+
3853
+ ### Consumer migration
3854
+ - **Consumer-noop.** `test/` is a repo gate, not a materialized surface.
3855
+ `CONSUMER_VERSION` stays at `3.5.0-rc.5`.
3856
+
3857
+ ## [3.5.0-rc.6] — 2026-08-20
3858
+
3859
+ > **AFM now suggests a guardrail by signal, with no new op and no new skill.** Phase 3 of the
3860
+ > active-executor plan. The catalog grows. The mechanism that consults it does not.
3861
+
3862
+ ### Added
3863
+
3864
+ - **Guardrail catalog** (`guardrails/*.md`) — **data, not a skill**. Each entry carries the `sinal`
3865
+ (the 0/1 command that detects whether **this** project would benefit), the `gatilho` (the gate's
3866
+ 0/1 once installed), the `superficie`s where it can run, and the `parametros` that **the
3867
+ operator** fills in. Three entries: executable import boundary, `never` exhaustiveness,
3868
+ mandatory order in a fluent chain.
3869
+ - **`## What would refute this` is now a mandatory field on every entry.** An installed guardrail
3870
+ is permanent friction, and friction with no visible rationale is the first thing someone removes
3871
+ on a busy Friday. This section states **what evidence would overturn the recommendation** — not
3872
+ "when not to use it", which is opinion, but the observable fact that makes the gate wrong for
3873
+ this project. Whoever removes a guardrail should be able to cite the line that refuted it.
3874
+ - **`interview/triggers.md` gains TWO lines, not one per guardrail.** The first runs the `sinal` of
3875
+ the catalog entries and proposes installation on a match; the second flags a hard rule whose
3876
+ trigger rewrites in prose what a versioned script already does. The catalog grows without
3877
+ touching the trigger — one line per guardrail would be the same sentence written N times.
3878
+ - **`ops/rule.md` gains steps 3.5 and 3.6.** Step 3.5 points the `*Verification:*` at the versioned
3879
+ script when one exists (two sources of truth for the same check drift out of sync on their own,
3880
+ and the one the human reads stops being the one that runs). Step 3.6 puts the **constant in the
3881
+ operator's hands**: measure the repo and propose it with the measurement attached, never hardcode
3882
+ a "best practice" value — a rule whose constant the owner did not choose is a rule they turn off
3883
+ on first friction.
3884
+
3885
+ ### Fixed
3886
+
3887
+ - **`ops/diagnose.md` A5 gains two sub-cases**, and the second is a paid lesson. **HIGH:** a trigger
3888
+ that duplicates a versioned script in prose. **CRITICAL:** a trigger that ends in `| head`,
3889
+ `| wc -l`, or any pipe whose last stage always exits `0` — **vacuously green**, it passes whether
3890
+ installed or not, and measures presence instead of activation. It happened twice in this line of
3891
+ work: once in a design the skeptic knocked down (`awk` with an empty `END{}` block), and once in
3892
+ the signals of the catalog above (`| head -1`), caught in dogfooding minutes after it was
3893
+ written. A trigger that never fails is worse than none: it spends the trust a real gate would
3894
+ need.
3895
+
3896
+ ### Not done, and why
3897
+
3898
+ - **A `guardrail` op and a new skill.** A skill costs ~550 chars of listing ≈ **~140 resident
3899
+ tokens in every session of every project**, including ones that will never install a single
3900
+ guardrail — on the order of the unit economics a single check brings. The surfaces that already
3901
+ exist (passive `reconcile`, `ops/rule.md`, `ops/diagnose.md` A5) cover the case with no permanent
3902
+ tax.
3903
+ - **Installing the gate** (CI, git hook, a target the operator names). Left for its own round: the
3904
+ op would then write **outside `docs/`** — into a build manifest and CI config — and that strains
3905
+ the architectural-change bright line. Suggesting is autonomous; installing is not.
3906
+ - **§ 3 becoming an installable catalog.** That would be a **regression**: today every hard rule is
3907
+ injected on every `SessionStart` and at write time, in every AFM project, **with no installation
3908
+ at all**. A project that skipped installation would end up with zero, not a weaker control. § 3
3909
+ **is** the suggestion layer; installation is the optional upgrade on top of it.
3910
+
3911
+ ### Consumer migration
3912
+ - **Consumer-noop.** Nothing materialized changes: `guardrails/` is read from the plugin by
3913
+ `reconcile`, `interview/triggers.md` is a protected surface that `update` never materializes
3914
+ (`ops/update.md` § 2), and `ops/*` are not materialized. `CONSUMER_VERSION` stays at `3.5.0-rc.5`.
3915
+
3916
+ ## [3.5.0-rc.5] — 2026-08-20
3917
+
3918
+ > **The framework gives the shape; the operator gives the value.** Two universal rules nailed
3919
+ > down a constant that never belonged to the framework — and one of them was the `domain_` that
3920
+ > started this whole line of work.
3921
+
3922
+ ### Fixed
3923
+
3924
+ - **Rules 6 and 7 nailed down an operator decision inside `core/`.** Rule 6 said `≤ 300 lines`
3925
+ and rule 7 prescribed the `domain_` prefix, both **literal** — next to `{{domain-path}}`,
3926
+ `{{ext}}`, and `{{schema-lib}}` in the same paragraph. This proves the omission was accidental,
3927
+ not a design choice. They became `{{limite-linhas}}` and `{{prefixo-dominio}}`, parametrized at
3928
+ the **7 points** where they appeared (the rule, `*Verification:*`, § 1.2 doctrine, anti-patterns,
3929
+ and the DoD checklist — parametrizing only the rule and leaving the DoD saying `300` would
3930
+ create an internal contradiction).
3931
+ - **`{{path}}` was a name orphaned from `{{domain-path}}`.** It appeared **only once in the whole
3932
+ template**, in rule 7's `*Verification:*`, while rules 15 and 16 use `{{domain-path}}` for the
3933
+ same directory. The consumer would have to fill in two names for one concept — or, more likely,
3934
+ fill in one and leave the other raw. Unified.
3935
+
3936
+ ### Added
3937
+
3938
+ - **The interview measures before it proposes** (discover-first, #2). `bootstrap.md` gains the
3939
+ constants step: it runs the real file-size distribution (p90) and detects by `grep` the function
3940
+ prefix the code **already** uses, and only then proposes the value **together with the
3941
+ measurement**. A greenfield project with no code to measure gets `300`/`domain_` **declared as
3942
+ the template default**, not as a measurement — the difference between an assumed default and a
3943
+ chosen one is what decides whether the rule survives first contact. An operator who does not
3944
+ answer gets `[TBD]` and a note that the rule stays without a trigger until someone fills it in
3945
+ (#4), never an invented number.
3946
+ - **The brownfield audit stops assuming the answer.** `refactor.md` A.5 filtered with
3947
+ `awk '$1 > 300'` — it used the constant that the command existed to **discover**. Now it prints
3948
+ the distribution and the constant comes from it, and a new `grep` detects the prefix convention
3949
+ already in use.
3950
+
3951
+ ### Consumer migration
3952
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.5.0-rc.3` → `3.5.0-rc.5`.
3953
+
3954
+ **There is NO Replacements table, and the omission is deliberate.** A find-and-replace swapping
3955
+ `300` for `{{limite-linhas}}` in the instance would leave an **unfilled placeholder** where a
3956
+ working number used to be — worse than the problem. The materialized instance of any earlier
3957
+ release has `300` and `domain_` **literal and working**; they are not wrong, they were just never
3958
+ chosen.
3959
+
3960
+ The migration is a **question**, not a substitution:
3961
+
3962
+ > Does your rule 6 say `≤ 300 lines` and rule 7 use `domain_`? Were these values **your choice**
3963
+ > or did they come from the template? If they came from the template, run the measurement in
3964
+ > `bootstrap.md § As constantes das regras` and decide. If they were your choice, there is nothing
3965
+ > to do — the number is already yours.
3966
+
3967
+ Detection (`0/1`) of an instance that still carries the inherited default with no decision on
3968
+ record:
3969
+
3970
+ ```sh
3971
+ grep -qE '≤ 300 linhas' docs/afm.md && ! grep -q 'limite-linhas' docs/afm.md && echo "herdado"
3972
+ ```
3973
+
3974
+ - **Plugin-only:** `interview/bootstrap.md`, `interview/refactor.md`.
3975
+
3976
+ ## [3.5.0-rc.4] — 2026-08-20
3977
+
3978
+ > **The gate spent more context saying everything was fine than failing.** Phase 0 of
3979
+ > the active-runner plan: the measured token savings, and its rationale protected against
3980
+ > whoever comes to "improve" it later.
3981
+
3982
+ ### Fixed
3983
+
3984
+ - **`afm-pre-edit.sh` told you to read a file without saying which one.** The learnings block labels
3985
+ *"read the whole file"*, but the `grep` ran without `-H` — and with **exactly one** file in
3986
+ `docs/learnings/` `grep` omits the name, delivering only `7:- **Trigger:** …`. The bug goes away
3987
+ on its own from the second learning on, which explains why it never showed up: **one-learning is the
3988
+ state of the first learning any project creates**, and it disappears right when someone
3989
+ would look. A new fixture covers the single-file case.
3990
+
3991
+ ### Added
3992
+
3993
+ - **Silent output on success in the mechanical gates** (`-v`/`V=1` restores it). Measured: the four
3994
+ gates totaled **10,099 chars per run** and **8,476 of them were the scripts saying everything was
3995
+ fine** — an `ok —` for each of the 36 skills and the 61 assertions, plus 50 section
3996
+ headers. After: **1,627 chars (−2,119 tokens per run)**, paid on every work session.
3997
+ The asymmetry is the whole design: **silent success, verbose failure** — a planted failure
3998
+ still prints `FAIL —` and the summary, and the summary never disappears in either mode.
3999
+
4000
+ ### Consumer migration
4001
+ - **Plugin-only.** `hooks/afm-pre-edit.sh`, `hooks/test/run.sh`, `hooks/test/smoke-v32.sh`.
4002
+ The repo gates (`test/*.sh`) are not materialized. `CONSUMER_VERSION` stays at
4003
+ `3.5.0-rc.3` — **consumer-noop**.
4004
+
4005
+ ## [3.5.0-rc.3] — 2026-08-20
4006
+
4007
+ > **Two mechanisms were ready but mute because of one character.** The `sig=` lost recurrence
4008
+ > to a path, and one whole hard rule vanished from context because of one asterisk. Neither
4009
+ > failed loudly — this release attacks that failure mode.
4010
+
4011
+ ### Fixed
4012
+
4013
+ - **The `sig=` almost never recurred, and rule 19 never fired.** The `ERRN` was the first 40
4014
+ chars of the raw normalized error — and in typical oracle output the **most volatile**
4015
+ tokens lead (path, `(line,column)`, failure count), while the stable discriminator comes
4016
+ later or gets cut off. The **same** `TS2322` in two files produced two signatures, so
4017
+ promotion by recurrence (>=2x) never happened. Evidence: this repo has **one** `sig` in
4018
+ three months of ledger. Now path, `line:column`, and isolated digits are dropped **before**
4019
+ truncation; `\b[0-9]+\b` only matches a bare digit, so `TS2322` and `TS2345` stay distinct.
4020
+ A new fixture covers both sides.
4021
+ - **A timeout counted as an oracle failure.** The only `sig` this repo recorded in three
4022
+ months was `typecheck-exit-code-143-command-timed-out-after-2m` — a timeout, which is a
4023
+ verdict about the **machine** (a slow suite, a stuck runner, the harness limit), not about
4024
+ the code. Same class as the `is_interrupt` the hook already excluded. A new guard covers
4025
+ exit 124/143 and "timed out". It deliberately does **not** cover the runner's per-test
4026
+ timeout (`timeout of Nms exceeded`): that one is a signal about the code and stays captured.
4027
+ - **Rule 7 never reached context — because of one asterisk.** The hooks' headline extractor
4028
+ uses `\*\*([^*]*)\*\*`, and rule 7's bold text contained `` `domain_*` ``: the `[^*]*` stops
4029
+ at the asterisk, the match fails, and the rule gets dropped **silently**. This is exactly
4030
+ the rule that separates pure domain from glue. The text became `` `domain_<nome>` ``, and —
4031
+ more important — a **class gate** entered the smoke test: the count of rules with a headline
4032
+ in § 3 has to match the count the extractor returns. Any future rule with `*` in its bold
4033
+ text now fails the test instead of vanishing without warning. Same failure as rule 13's
4034
+ headline (rc.2), now with a trigger.
4035
+ - **The § 3 preamble promised more than it delivered.** "Every rule below has an executable
4036
+ trigger" was false for rules **1** and **12**, whose `*Verification:*` was prose — and the
4037
+ preamble itself says a rule with no binary trigger is not a hard rule. Both gained the
4038
+ mechanizable half (`git diff --staged --name-only | grep -qE '\.(test|spec)\.'` for rule 1;
4039
+ a grep for the rerun case for rule 12) and now **state explicitly** which half the command
4040
+ proves and which half stays judgment — including the false positive on a pure refactor,
4041
+ rename, revert, and config commit. The preamble now acknowledges the partial trigger instead
4042
+ of pretending full coverage. Precedent: same honesty that demoted rules 3, 8, 9, and 14 to
4043
+ principles.
4044
+
4045
+ ### Consumer migration
4046
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.5.0-rc.2` → `3.5.0-rc.3`.
4047
+
4048
+ **Replacements** (apply in `<consumer>/docs/`):
4049
+
4050
+ | File | From | To |
4051
+ | --- | --- | --- |
4052
+ | `docs/afm.md` | `` UMA função `domain_*`. `` | `` UMA função `domain_<nome>`. `` |
4053
+ | `docs/afm.md` | `*Verification:* test runner cobre o caminho novo; diff mostra` | `*Verification:* \`git diff --staged --name-only \| grep -qE '\.(test\|spec)\.'\` — o commit que traz código traz teste junto; o comando prova que existe teste, não que ele cobre o caminho novo. Antes dizia: test runner cobre o caminho novo; diff mostra` |
4054
+
4055
+ > The rule 7 replacement is the one that matters: without it the instance keeps the rule
4056
+ > invisible to `SessionStart`. Run `awk '/^## 3\./{f=1} /^## [0-9]/&&!/^## 3\./{if(f)f=0} f' docs/afm.md
4057
+ > \| sed -nE 's/^([0-9]+)\. \*\*([^*]*)\*\*.*/\1/p' \| wc -l` and compare with
4058
+ > `grep -cE '^[0-9]+\. \*\*'` over the same slice — the two must match.
4059
+
4060
+ - **Plugin-only:** `hooks/afm-capture-failure.sh`, `hooks/test/run.sh`.
4061
+
4062
+ > **The `sig=` contract.** This change alters the **recurrence key** that `remediate`,
4063
+ > `reconcile`, `evolve`, rule 19, and the `failure-classification` rubric consume. Signatures
4064
+ > already recorded in an existing ledger are not rewritten (rule 18 — append-only, and `update`
4065
+ > never deletes): they simply stop matching the new ones. The practical effect is a recurrence
4066
+ > window that restarts from zero, and no lost promotion — the old one almost never matched,
4067
+ > which is the bug.
4068
+
4069
+ ## [3.5.0-rc.2] — 2026-08-20
4070
+
4071
+ > **A rubric asserted a guarantee the compiler does not give.** Triage round on an
4072
+ > external report of 12 gaps: 6 fell on recheck, 4 survived shrunk, and the costliest
4073
+ > one was the one nobody had looked at — a false sentence copied verbatim into every instance.
4074
+
4075
+ ### Fixed
4076
+
4077
+ - **`rubrics/error-classification.md` promised what TypeScript does not deliver.** The line said
4078
+ *"exhaustive switch guarantees coverage at the caller"*. It does not: a union of 3 codes with a switch
4079
+ covering 2 compiles clean under `tsc --noEmit --strict` (exit 0). The guarantee only exists if the
4080
+ `default` assigns the rest to `never`. Both examples got closed with the assertion (the one in the
4081
+ Procedure was an `if`-chain with no `else` — it became a `switch`, one form at both levels, because
4082
+ that is where hard rule 15 supervises the code → transport mapping), and a new anti-pattern names the
4083
+ case. The sentence was not an omission: it was a false claim about compiler behavior.
4084
+ - **`core/protocol.md § 6` announced enforcement that does not exist.** The "hard" line listed
4085
+ `RETRY_CAP`, `ROUND_CAP`, `MAX_PARALLEL_SUBAGENTS`, and `permissions.deny` as imposed by the harness —
4086
+ `grep` for those names in `hooks/`: **zero hits**, and it is architecturally impossible (the golden
4087
+ rule forbids `decision:"block"`, and fan-out goes out in one message before the first `SubagentStart`).
4088
+ The two-half taxonomy was what fell short: now there are **three** — **Hard** (real external
4089
+ mechanism: cadence counters + 0/1 gates in `test/`), **Counted** (counted and observable, nothing
4090
+ blocks: `RETRY_CAP`/`QUESTION_CAP` are self-observable within their own window — a hook there would be
4091
+ YAGNI; `MAX_PARALLEL_SUBAGENTS` is only reportable *after the fact*; `ROUND_CAP`, whose note "it is a
4092
+ counter, not a promise" was already the prototype of the category) and **Soft**. `permissions.deny`
4093
+ moved down to a footnote: it is a harness mechanism class, not something AFM configures.
4094
+ - **The hard-rule-13 headline lost `docs/sessions/` on the way into context.** `afm-session-start.sh`
4095
+ and `afm-subagent-start.sh` crop each rule's headline at the closing `**` — the directory
4096
+ was in the body, so what arrived was *"13. Tokens and secrets never leak."*, without the target. Since
4097
+ the versioned session narrative is exactly where a secret leaks by accident, the directory moved into
4098
+ the headline. Zero hook touched, zero regex duplicated.
4099
+ - **`core/afm.md § 4.3` told the agent to write the missing test without saying which one.** Now it names
4100
+ the **characterization test**: it records the **CURRENT** observed behavior, never the desired one —
4101
+ the real failure mode is the agent cementing a bug while testing what it thinks is correct.
4102
+ - **`rubrics/when-to-create-dsl.md` chose the form by cost and never by correctness.** Four forms
4103
+ ranked by price, four self-critique questions, none about illegal order. A 5th one came in
4104
+ (mandatory order → refuse at compile-time via type-state, **or do not propose the builder** — a single
4105
+ function with required arguments solves the same thing without the technique) plus a purity note in
4106
+ item 1: a chain builds an immutable value, IO in one single terminal step. An effect mid-chain forces
4107
+ mocking the world to test a pure rule, which is what hard rule 7 separates, and a builder outside
4108
+ `src/<domain>/` escapes it.
4109
+ - **`ops/research.md` did not plan for the tool simply not existing.** The `WebFetch` fallback covered
4110
+ `no_match` and a non-HTML target, but not "the MCP did not come up in this session" (build failed, no
4111
+ Node, restricted environment) — a case where the error dies in the process's stderr and never reaches
4112
+ the context. Now it is a named case, not an agent error, and the produced artifact carries the mark
4113
+ `> MCP unavailable this session — WebFetch mode`.
4114
+ - **Four references taught the wrong number** (own commit, wave 0): `focus.template.md` cited hard rules
4115
+ 21 and 22, which do not exist (the real one is 20); `interview/bootstrap.md` told the agent to number
4116
+ project rules "starting at nº 17" against `core/afm.md`, which reserves 18-29 for the maintainer and
4117
+ tells a project to start at **30**; the `afm-capture.sh` header declared matcher `Edit|Write` while
4118
+ `hooks.json` says `Edit|Write|Bash`; `hooks/README.md` said "6 scripts" (there are 7) and "Four events"
4119
+ (there are 6 — it was missing `SubagentStart` and `PostToolUseFailure`).
4120
+
4121
+ ### Added
4122
+
4123
+ - **A distrust envelope on the `research` MCP's output** (`mcp/src/server.ts`). The `web_read` return is
4124
+ the only point where third-party content enters the context, and since `ops/research.md` demoted native
4125
+ `WebFetch` to a last resort, it became the **default** channel. BM25 makes it worse: it ranks by match
4126
+ against the agent's own query, so whatever the agent is looking for gets promoted to the top of the
4127
+ distilled excerpt. The content now arrives prefixed as data, not instruction — factual, non-imperative
4128
+ phrasing, same language as `afm-pre-edit.sh`.
4129
+
4130
+ ### Not done, and why
4131
+
4132
+ - **Destination validation (SSRF) in `mcp/src/fetch.ts` — a deliberate non-decision.** The proposal was
4133
+ an `isUrlAllowed` rejecting loopback, link-local, and RFC 1918. Rejected for two reasons, in this
4134
+ order: (1) **there is no privilege boundary to protect** — the MCP runs as `node bootstrap.mjs` under
4135
+ the same user and the same network stack as the agent, which already has Bash; (2) the guard has a
4136
+ **verified bypass**: `http://[::ffff:127.0.0.1]/` reaches Node as hostname `[::ffff:7f00:1]` and matches
4137
+ none of the proposed regexes (same for `[0:0:0:0:0:ffff:a00:1]`). And the realistic vector —
4138
+ exfiltration to `https://attacker.com/?d=<secret>` — stays open by construction. A decorative mechanism
4139
+ costs the trust of every neighboring mechanism; the strictly correct form would be `undici` with a
4140
+ custom `connect`, which pays for a new dependency in a package that today has a single one. Recorded
4141
+ as a choice, not an oversight.
4142
+ - **A degradation contract in `mcp/bootstrap.mjs`.** The proposed acceptance criterion (`echo $?` → 1)
4143
+ **already passes today** (`ERR_MODULE_NOT_FOUND`), and the diagnostic channel already exists:
4144
+ `stdio: [...,"inherit"]` inherits `npm install`/`tsc` stderr — the same place the proposed
4145
+ `console.error` would land, and neither one reaches the model's context. What was left is the side the
4146
+ model does see, which became the `ops/research.md` paragraph above. The proposed `docs/.afm-version`
4147
+ gate was worse: `process.exit(0)` with no handshake makes the client show "research: failed" in every
4148
+ non-AFM project.
4149
+ - **Materializing `.husky/pre-push` in the consumer from § 6.1.** Neither husky nor `.githooks/` creates
4150
+ a trigger without per-clone wiring (`prepare` + `npm install`; `git config core.hooksPath`, an
4151
+ unversioned config) — and the proposed 0/1 (`git ls-files .husky/pre-push | wc -l` → 1) stays **green
4152
+ with the hook inert**: it measures presence, not activation. Same class as the denominator bug fixed in
4153
+ rc.1. § 6.1 also is not left unguarded already: `PRINCIPLES.md` bright line (c) and `ops/deliver.md`
4154
+ stop at the push boundary.
4155
+ - **A `kind` of process in the ledger to show RED before GREEN.** The premise was false:
4156
+ `afm-capture-failure.sh` already records `failure | sig=test-…` with promotion on recurrence. The two
4157
+ proposed `kind` values would be write-only (`afm-health.sh` counts only `deliverable`/`task`),
4158
+ `test-modified-same-commit` fires on a **well-formed** TDD commit — the good case — and
4159
+ `task-closed-no-red-signal` is scoped per session, not per task: one RED at any point would whitewash
4160
+ every following commit.
4161
+ - **Marking a failure→success adjacency as `sig=flaky-`.** Failing and then passing right after is TDD's
4162
+ happy path; the heuristic would poison hard rule 19 with signatures nobody should remediate. The
4163
+ doctrine already covers the case (`core/afm.md § 1.2`, "a flaky test becomes tech debt").
4164
+ - **A debt ratchet for brownfield.** The floor already exists in prose (`interview/refactor.md`:
4165
+ boy-scout — new code **and modified file**), `≤ baseline` would authorize eternal stagnation, and the
4166
+ blocker is mechanical: every command cited under `*Verification:*` is a **global** repo count and none
4167
+ accepts a target, so "no regression in the touched file" is unimplementable as written.
4168
+ - **Evals for `deliver`/`reflect`/`evolve`.** The selection of the 7 covered ops is **measured and
4169
+ documented** (`test/lib/eval-verdict.py`: "ops that had usageCount 0 in the 3.1.0-rc.6 baseline";
4170
+ `deliver` = 53 uses). The suite is a discoverability death-list, not behavior coverage — and the
4171
+ `tool_used` grader over a 20-step op measures invocation, not conduct.
4172
+ - **A hard rule for protected branches.** It does not become a universal: many solo projects work on
4173
+ `main` on purpose, and AFM avoids imposing topology. The finding went where a vehicle already existed —
4174
+ the bootstrap free-text question gained the ready-made example and trigger, and refactor's A.7 now
4175
+ reads recurring merges / `CODEOWNERS` as a PR-flow signal. The rule is born in the consumer's slot
4176
+ **30**, by the consumer's own choice.
4177
+
4178
+ ### Consumer migration
4179
+ - **Consumer-facing.** `CONSUMER_VERSION`: `3.4.0-rc.7` → `3.5.0-rc.2`.
4180
+
4181
+ **Re-copy** (the delta is *insertions*, which do not fit a From→To table):
4182
+
4183
+ | Consumer file | Action |
4184
+ | --- | --- |
4185
+ | `docs/rubrics/error-classification.md` | re-copy from `rubrics/error-classification.md` |
4186
+ | `docs/rubrics/when-to-create-dsl.md` | re-copy from `rubrics/when-to-create-dsl.md` |
4187
+
4188
+ > If the instance edited the rubric locally, `update` **proposes** the diff instead of overwriting — a
4189
+ > blind re-copy would violate rule 17 (anti-context-collapse) on a doc the consumer may have extended.
4190
+
4191
+ **Replacements** (apply in `<consumer>/docs/`):
4192
+
4193
+ | File | From | To |
4194
+ | --- | --- | --- |
4195
+ | `docs/afm.md` | `13. **Tokens e segredos não vazam.**` | `13. **Tokens e segredos não vazam — inclusive na narrativa de \`docs/sessions/\`.**` |
4196
+ | `docs/afm.md` | `numa área que vai mexer, escreve antes.` | `numa área que vai mexer, escreve um **characterization test**: grava o comportamento **ATUAL** observado (roda e captura a saída real), nunca o desejado.` |
4197
+ | `docs/_focus.md` | `regra dura 22` | `regra dura 20` |
4198
+ | `docs/_focus.md` | `anti-context-collapse, regra 21` | `anti-context-collapse` |
4199
+
4200
+ - **Plugin-only** (not materialized, nothing to do in the instance): `core/protocol.md` (the consumer
4201
+ materializes `docs/protocols/p{1,2,3}.md`, which do not repeat the taxonomy), `interview/*` (only
4202
+ affects **new** bootstrap/refactor), `ops/research.md`, `mcp/`, `hooks/`.
4203
+
4204
+ ## [3.5.0-rc.1] — 2026-07-26
4205
+
4206
+ > **Fan-out gets a ceiling, and approved scope gets a floor.** Two gaps that the Claude 5
4207
+ > generation widens: it delegates more spontaneously than the previous one and it tends to widen
4208
+ > scope. Both turn into a number or a rule instead of prose.
4209
+
4210
+ ### Added
4211
+
4212
+ - **Parameter `MAX_PARALLEL_SUBAGENTS`** (`core/protocol.md § 2`; default `4`, P1 `4`, P2 `2`, P3 `6`).
4213
+ It bites in `ops/research.md` step 4 and `ops/deliver.md` 0.C — the only two fan-out sites in AFM,
4214
+ both **without a ceiling** until now. Above the limit the instruction is **prioritize, not slice**: run
4215
+ the N that move the decision most and log the rest as `[NEEDS CLARIFICATION:]`, because slicing into
4216
+ rounds pays the spawn cost twice for the same signal.
4217
+ - **`core/protocol.md § 5` gets a second dimension.** The `delegation` axis always governed *what* gets
4218
+ delegated (`none`/`read-only`/`full`) and never *how many* — `read-only` with no ceiling could still
4219
+ fire N parallel searches. The two dimensions are now declared as independent.
4220
+ - **Scope block in `ops/deliver.md` Phase 3.** `tasks.md` is a ceiling, not a floor: the REFACTOR step
4221
+ applies to the code the task touched, not to code that was nearby; narrowing scope is as much a
4222
+ violation as widening it (reporting "done" with half the work finished is the symmetric failure); a
4223
+ change of direction becomes **one sentence, then continue with the approved task** — resizing the task
4224
+ is the owner's decision. This is the execution-side counterpart of #6, symmetric to the gate: the gate
4225
+ controls what enters scope, this controls that it does not grow on its own afterward.
4226
+
4227
+ ### Fixed
4228
+
4229
+ - **`test/skills-lint.sh` measured the wrong denominator.** The baseline was named
4230
+ `listing_total_all_plugins` but it summed only the three plugins **in this repo** — and the listing
4231
+ budget is **one budget, shared by every installed plugin plus `~/.claude/skills`**. That is how `seo`
4232
+ started to render **5 entries with no description** (`gaps`, `index`, `pseo`, `watch`, `write` —
4233
+ exactly the ones with `usageCount: 0`) while the gate stayed green: the toolkit shrank, the neighbor
4234
+ grew, and nothing measured the sum that matters. Same class as G3 — a gate that measures a proxy
4235
+ instead of the real thing. The key became `listing_total_toolkit` (the old name was a lie) and
4236
+ `test/lib/listing-shared.py` + `gate_listing_shared` were added, **advisory by design**: the installed
4237
+ set is local to the machine, so failing there would fail the environment, not the commit. Measured
4238
+ here: 25,815 total chars, of which 6,070 belong to a plugin outside the toolkit.
4239
+
4240
+ ### Not done, and why
4241
+
4242
+ - **We did not remove the verification directives from the ops.** Opus 5 migration guidance says to
4243
+ delete self-check scaffolding ("check your answer") because the model already verifies on its own and
4244
+ instructing it causes over-verification. Audited: of the 29 occurrences of "verification" in the ops,
4245
+ **25 are `*Verification:*` lines** — the executable 0/1 oracle the hard rule requires, which is
4246
+ **external signal**, not self-judgment. A search targeted at real scaffolding (self-check, a dedicated
4247
+ verifier sub-agent) returns **zero**. Principle #5 already covered this discipline; deleting those
4248
+ triggers would gut rule 3 (mechanize-or-demote) with no gain. Logged here because the next audit will
4249
+ run the same grep and deserves the ready answer.
4250
+
4251
+ ### Consumer migration
4252
+ - **`MAX_PARALLEL_SUBAGENTS` is the only row in the table whose `default` does NOT reproduce
4253
+ pre-v4 behavior** — fan-out was unlimited before. If you depend on firing more than 4 sub-agents in a
4254
+ single turn, declare the value in the project's protocol; otherwise no action is needed.
4255
+ - **Run `/afm:update`** to materialize the new row in your `docs/` parameter table and the scope block
4256
+ in Phase 3.