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

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