@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.
- package/bin/afm +4 -0
- package/content/.coupling-baseline.tsv +1 -0
- package/content/.pending-commands.txt +2 -0
- package/content/CHANGELOG-archive.md +806 -0
- package/content/CHANGELOG.md +4320 -0
- package/content/PRINCIPLES.md +119 -0
- package/content/README-light.md +158 -0
- package/content/README.md +150 -0
- package/content/code-style/_any/file-size.md +62 -0
- package/content/code-style/_any/imports.md +68 -0
- package/content/code-style/_any/naming.md +72 -0
- package/content/code-style/_any/test-placement.md +57 -0
- package/content/code-style/nextjs-app-router/imports.md +50 -0
- package/content/code-style/nextjs-app-router/naming.md +50 -0
- package/content/code-style/prisma-postgres/error-boundary.md +55 -0
- package/content/code-style/prisma-postgres/imports.md +61 -0
- package/content/code-style/shadcn-ui/naming.md +48 -0
- package/content/code-style/stripe-billing/naming.md +76 -0
- package/content/code-style/trigger-dev-v4/imports.md +58 -0
- package/content/code-style/trigger-dev-v4/naming.md +49 -0
- package/content/code-style/trigger-dev-v4/test-placement.md +61 -0
- package/content/code-style/trpc-v11/error-boundary.md +51 -0
- package/content/code-style/trpc-v11/file-size.md +44 -0
- package/content/code-style/trpc-v11/naming.md +60 -0
- package/content/code-style/trpc-v11/test-placement.md +52 -0
- package/content/core/ach.md +260 -0
- package/content/core/afm.md +514 -0
- package/content/core/gotchas.md +18 -0
- package/content/core/prd.md +141 -0
- package/content/core/protocol.md +158 -0
- package/content/core/ust.md +65 -0
- package/content/guardrails/README.md +55 -0
- package/content/guardrails/dsl-com-ordem-obrigatoria.md +47 -0
- package/content/guardrails/exaustividade-de-union.md +48 -0
- package/content/guardrails/fronteira-de-import.md +57 -0
- package/content/hooks/README.md +70 -0
- package/content/interview/bootstrap.md +417 -0
- package/content/interview/promotion.md +115 -0
- package/content/interview/refactor.md +379 -0
- package/content/interview/triggers.md +107 -0
- package/content/modules/nextjs-app-router/ach.patch.md +44 -0
- package/content/modules/nextjs-app-router/gotchas.seed.md +78 -0
- package/content/modules/nextjs-app-router/rules.patch.md +35 -0
- package/content/modules/prisma-postgres/ach.patch.md +65 -0
- package/content/modules/prisma-postgres/rules.patch.md +28 -0
- package/content/modules/shadcn-ui/ach.patch.md +42 -0
- package/content/modules/stripe-billing/ach.patch.md +46 -0
- package/content/modules/stripe-billing/gotchas.seed.md +73 -0
- package/content/modules/stripe-billing/rules.patch.md +30 -0
- package/content/modules/trigger-dev-v4/ach.patch.md +64 -0
- package/content/modules/trigger-dev-v4/gotchas.seed.md +53 -0
- package/content/modules/trigger-dev-v4/rules.patch.md +35 -0
- package/content/modules/trpc-v11/ach.patch.md +56 -0
- package/content/modules/trpc-v11/rules.patch.md +34 -0
- package/content/modules/vireya-ui/ach.patch.md +85 -0
- package/content/modules/vireya-ui/gotchas.seed.md +98 -0
- package/content/modules/vireya-ui/rules.patch.md +36 -0
- package/content/ops/adr.md +54 -0
- package/content/ops/analyze.md +130 -0
- package/content/ops/clarify.md +86 -0
- package/content/ops/deliver.md +209 -0
- package/content/ops/diagnose.md +143 -0
- package/content/ops/evolve.md +366 -0
- package/content/ops/export-template.md +97 -0
- package/content/ops/generalize.md +117 -0
- package/content/ops/gotcha.md +46 -0
- package/content/ops/plan.md +61 -0
- package/content/ops/policy.md +61 -0
- package/content/ops/promote.md +72 -0
- package/content/ops/protocol.md +51 -0
- package/content/ops/reconcile.md +44 -0
- package/content/ops/reflect.md +182 -0
- package/content/ops/remediate.md +143 -0
- package/content/ops/restructure.md +142 -0
- package/content/ops/rule.md +170 -0
- package/content/ops/specify.md +62 -0
- package/content/ops/tasks.md +67 -0
- package/content/ops/update.md +252 -0
- package/content/ops/verify.md +67 -0
- package/content/patterns/adapter.md +46 -0
- package/content/patterns/facade.md +47 -0
- package/content/patterns/factory.md +46 -0
- package/content/patterns/null-object.md +55 -0
- package/content/patterns/result-vs-exception.md +52 -0
- package/content/patterns/strategy.md +45 -0
- package/content/patterns/template-method-vs-composition.md +50 -0
- package/content/questions/adr.questions.yaml +14 -0
- package/content/questions/analyze.questions.yaml +12 -0
- package/content/questions/bootstrap-mapping.questions.yaml +14 -0
- package/content/questions/bootstrap-rules.questions.yaml +10 -0
- package/content/questions/bootstrap-stack.questions.yaml +23 -0
- package/content/questions/clarify.questions.yaml +10 -0
- package/content/questions/code-style.questions.yaml +12 -0
- package/content/questions/confirm.questions.yaml +12 -0
- package/content/questions/deliver.questions.yaml +23 -0
- package/content/questions/evolve.questions.yaml +15 -0
- package/content/questions/export-template-apply.questions.yaml +11 -0
- package/content/questions/export-template.questions.yaml +23 -0
- package/content/questions/gates.questions.yaml +27 -0
- package/content/questions/generalize.questions.yaml +13 -0
- package/content/questions/gotcha.questions.yaml +11 -0
- package/content/questions/init-template.questions.yaml +10 -0
- package/content/questions/init.questions.yaml +41 -0
- package/content/questions/plan.questions.yaml +14 -0
- package/content/questions/refactor.questions.yaml +23 -0
- package/content/questions/reflect.questions.yaml +13 -0
- package/content/questions/remediate.questions.yaml +27 -0
- package/content/questions/restructure.questions.yaml +15 -0
- package/content/questions/rf-mapping.questions.yaml +12 -0
- package/content/questions/rule-scope.questions.yaml +12 -0
- package/content/questions/rule.questions.yaml +12 -0
- package/content/questions/specify.questions.yaml +23 -0
- package/content/questions/update-artifacts.questions.yaml +10 -0
- package/content/questions/update-strings.questions.yaml +14 -0
- package/content/questions/update.questions.yaml +10 -0
- package/content/questions/verify.questions.yaml +9 -0
- package/content/roles/executor.md +18 -0
- package/content/roles/explorer.md +22 -0
- package/content/roles/reviewer.md +23 -0
- package/content/rubrics/enum-vs-union-vs-branded.md +40 -0
- package/content/rubrics/episodic-vs-semantic-boundary.md +37 -0
- package/content/rubrics/error-classification.md +98 -0
- package/content/rubrics/failure-classification.md +111 -0
- package/content/rubrics/negative-filters.md +38 -0
- package/content/rubrics/solid-triggers.md +119 -0
- package/content/rubrics/template-vs-streaming-precedence.md +34 -0
- package/content/rubrics/validation-boundary.md +94 -0
- package/content/rubrics/when-to-create-dsl.md +65 -0
- package/content/rubrics/when-to-create-lib.md +49 -0
- package/content/rubrics/when-to-create-module.md +39 -0
- package/content/rubrics/when-to-evolve-methodology.md +62 -0
- package/content/skills/adr.md +42 -0
- package/content/skills/analyze.md +28 -0
- package/content/skills/clarify.md +28 -0
- package/content/skills/deliver.md +26 -0
- package/content/skills/diagnose.md +42 -0
- package/content/skills/evolve.md +43 -0
- package/content/skills/export-template.md +37 -0
- package/content/skills/generalize.md +31 -0
- package/content/skills/gotcha.md +40 -0
- package/content/skills/init.md +77 -0
- package/content/skills/plan.md +28 -0
- package/content/skills/policy.md +25 -0
- package/content/skills/promote.md +39 -0
- package/content/skills/protocol.md +41 -0
- package/content/skills/reconcile.md +45 -0
- package/content/skills/refactor.md +79 -0
- package/content/skills/reflect.md +43 -0
- package/content/skills/remediate.md +33 -0
- package/content/skills/restructure.md +46 -0
- package/content/skills/rule.md +43 -0
- package/content/skills/specify.md +28 -0
- package/content/skills/tasks.md +24 -0
- package/content/skills/update.md +58 -0
- package/content/skills/verify.md +29 -0
- package/content/templates/adr.template.md +51 -0
- package/content/templates/evolution-report.template.md +132 -0
- package/content/templates/feature-plan.template.md +84 -0
- package/content/templates/feature-research.template.md +56 -0
- package/content/templates/feature-spec.template.md +86 -0
- package/content/templates/feature-tasks.induced.template.md +91 -0
- package/content/templates/feature-tasks.template.md +82 -0
- package/content/templates/focus.template.md +30 -0
- package/content/templates/gotcha.template.md +31 -0
- package/content/templates/guardrail-runner.template.md +97 -0
- package/content/templates/guardrail.template.md +63 -0
- package/content/templates/learning.template.md +40 -0
- package/content/templates/procedure.template.md +47 -0
- package/content/templates/protocols/p1.md +29 -0
- package/content/templates/protocols/p2.md +28 -0
- package/content/templates/protocols/p3.md +47 -0
- package/content/templates/research.template.md +50 -0
- package/content/templates/session-handoff.template.md +58 -0
- package/content/templates/us.template.md +39 -0
- package/content/tools/gate/core-rules/PLUGIN-13-secrets.sh +36 -0
- package/content/tools/gate/core-rules/PLUGIN-21-code-language.sh +27 -0
- package/content/tools/gate/core-rules/PLUGIN-35-gate-complete.sh +19 -0
- package/content/tools/gate/core-rules/PLUGIN-36-absolute-path.sh +38 -0
- package/content/tools/gate/core-rules/PLUGIN-37-prose-language.sh +56 -0
- package/content/tools/gate/core-rules/PLUGIN-38-parser-marker.sh +82 -0
- package/content/tools/gate/core-rules/PLUGIN-39-frontmatter-ops.sh +325 -0
- package/content/tools/gate/core-rules/PLUGIN-40-dead-reference.sh +74 -0
- package/content/tools/gate/core-rules/README.md +55 -0
- package/content/tools/gate/guardrails.sh +740 -0
- package/content/tools/gate/lib/ratchet.sh +119 -0
- package/content/tools/gate/lib/scope.sh +85 -0
- package/content/tools/gate/ratchet.seed.tsv +1 -0
- package/content/tools/gate/rules/README.md +50 -0
- package/package.json +19 -5
- package/plugin/agy/hooks.json +29 -0
- package/plugin/agy/mcp_config.json +7 -0
- package/plugin/agy/plugin.json +6 -0
- package/plugin/agy/skills/afm-adr/SKILL.md +48 -0
- package/plugin/agy/skills/afm-analyze/SKILL.md +34 -0
- package/plugin/agy/skills/afm-clarify/SKILL.md +34 -0
- package/plugin/agy/skills/afm-deliver/SKILL.md +30 -0
- package/plugin/agy/skills/afm-diagnose/SKILL.md +46 -0
- package/plugin/agy/skills/afm-evolve/SKILL.md +49 -0
- package/plugin/agy/skills/afm-export-template/SKILL.md +43 -0
- package/plugin/agy/skills/afm-generalize/SKILL.md +37 -0
- package/plugin/agy/skills/afm-gotcha/SKILL.md +46 -0
- package/plugin/agy/skills/afm-init/SKILL.md +81 -0
- package/plugin/agy/skills/afm-plan/SKILL.md +34 -0
- package/plugin/agy/skills/afm-policy/SKILL.md +29 -0
- package/plugin/agy/skills/afm-promote/SKILL.md +43 -0
- package/plugin/agy/skills/afm-protocol/SKILL.md +45 -0
- package/plugin/agy/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/agy/skills/afm-refactor/SKILL.md +83 -0
- package/plugin/agy/skills/afm-reflect/SKILL.md +49 -0
- package/plugin/agy/skills/afm-remediate/SKILL.md +39 -0
- package/plugin/agy/skills/afm-restructure/SKILL.md +52 -0
- package/plugin/agy/skills/afm-rule/SKILL.md +49 -0
- package/plugin/agy/skills/afm-specify/SKILL.md +34 -0
- package/plugin/agy/skills/afm-tasks/SKILL.md +28 -0
- package/plugin/agy/skills/afm-update/SKILL.md +64 -0
- package/plugin/agy/skills/afm-verify/SKILL.md +35 -0
- package/plugin/claude-code/.claude-plugin/marketplace.json +15 -0
- package/plugin/claude-code/.claude-plugin/plugin.json +9 -0
- package/plugin/claude-code/.mcp.json +7 -0
- package/plugin/claude-code/agents/afm-executor.md +18 -0
- package/plugin/claude-code/agents/afm-explorer.md +20 -0
- package/plugin/claude-code/agents/afm-reviewer.md +21 -0
- package/plugin/claude-code/hooks/hooks.json +85 -0
- package/plugin/claude-code/skills/adr/SKILL.md +48 -0
- package/plugin/claude-code/skills/analyze/SKILL.md +34 -0
- package/plugin/claude-code/skills/clarify/SKILL.md +34 -0
- package/plugin/claude-code/skills/deliver/SKILL.md +30 -0
- package/plugin/claude-code/skills/diagnose/SKILL.md +46 -0
- package/plugin/claude-code/skills/evolve/SKILL.md +49 -0
- package/plugin/claude-code/skills/export-template/SKILL.md +43 -0
- package/plugin/claude-code/skills/generalize/SKILL.md +37 -0
- package/plugin/claude-code/skills/gotcha/SKILL.md +46 -0
- package/plugin/claude-code/skills/init/SKILL.md +81 -0
- package/plugin/claude-code/skills/plan/SKILL.md +34 -0
- package/plugin/claude-code/skills/policy/SKILL.md +29 -0
- package/plugin/claude-code/skills/promote/SKILL.md +43 -0
- package/plugin/claude-code/skills/protocol/SKILL.md +45 -0
- package/plugin/claude-code/skills/reconcile/SKILL.md +49 -0
- package/plugin/claude-code/skills/refactor/SKILL.md +83 -0
- package/plugin/claude-code/skills/reflect/SKILL.md +49 -0
- package/plugin/claude-code/skills/remediate/SKILL.md +39 -0
- package/plugin/claude-code/skills/restructure/SKILL.md +52 -0
- package/plugin/claude-code/skills/rule/SKILL.md +49 -0
- package/plugin/claude-code/skills/specify/SKILL.md +34 -0
- package/plugin/claude-code/skills/tasks/SKILL.md +28 -0
- package/plugin/claude-code/skills/update/SKILL.md +64 -0
- package/plugin/claude-code/skills/verify/SKILL.md +35 -0
- package/plugin/opencode/afm/index.js +245 -0
- package/plugin/opencode/afm/tui.js +21 -0
- package/plugin/opencode/agents/afm-executor.md +39 -0
- package/plugin/opencode/agents/afm-explorer.md +92 -0
- package/plugin/opencode/agents/afm-reviewer.md +93 -0
- package/plugin/opencode/skills/afm-adr/SKILL.md +48 -0
- package/plugin/opencode/skills/afm-analyze/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-clarify/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-deliver/SKILL.md +30 -0
- package/plugin/opencode/skills/afm-diagnose/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-evolve/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-export-template/SKILL.md +43 -0
- package/plugin/opencode/skills/afm-generalize/SKILL.md +37 -0
- package/plugin/opencode/skills/afm-gotcha/SKILL.md +46 -0
- package/plugin/opencode/skills/afm-init/SKILL.md +81 -0
- package/plugin/opencode/skills/afm-plan/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-policy/SKILL.md +29 -0
- package/plugin/opencode/skills/afm-promote/SKILL.md +43 -0
- package/plugin/opencode/skills/afm-protocol/SKILL.md +45 -0
- package/plugin/opencode/skills/afm-reconcile/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-refactor/SKILL.md +83 -0
- package/plugin/opencode/skills/afm-reflect/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-remediate/SKILL.md +39 -0
- package/plugin/opencode/skills/afm-restructure/SKILL.md +52 -0
- package/plugin/opencode/skills/afm-rule/SKILL.md +49 -0
- package/plugin/opencode/skills/afm-specify/SKILL.md +34 -0
- package/plugin/opencode/skills/afm-tasks/SKILL.md +28 -0
- package/plugin/opencode/skills/afm-update/SKILL.md +64 -0
- package/plugin/opencode/skills/afm-verify/SKILL.md +35 -0
- 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.
|