devflow-kit 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
package/CHANGELOG.md CHANGED
@@ -5,6 +5,233 @@ All notable changes to Devflow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.0.0] - 2026-09-30
9
+
10
+ ### Added
11
+
12
+ - **The security deny list blocks OrbStack control, docker image, container and volume removal, docker pulls and prunes, and whole-disk docker mounts** ([#399](https://github.com/dean0x/devflow/issues/399)) — 15 new entries. An agent had restarted every OrbStack machine with `orbctl restart --all` and left `docker pull` runs hanging. `orb`, `orbctl` and `open …OrbStack…` are now denied, and so are `docker pull` / `docker image pull`, `docker rm` / `docker container rm` / `docker rmi` / `docker image rm` / `docker volume rm`, every `docker … prune`, and any `docker run` that carries `--privileged` or mounts the root filesystem (`-v /:`, `--volume /:`, `--volume=/:`), wherever the flag sits in the command. Everyday docker work stays allowed: `docker ps`, `docker build`, `docker logs`, `docker compose` and `docker run` with ordinary project mounts. Plain `curl` and `wget` stay allowed too. With the piped-shell fix below, the list goes from 154 to 170 entries. Re-run `devflow init` to apply them. Every new entry is recorded in devflow's historical deny set, so `devflow security --disable` and `devflow uninstall` recognise it as devflow's.
13
+ - **A committed `.devflow/project.json` for team-wide settings** ([#392](https://github.com/dean0x/devflow/issues/392)). A repository can now commit one file in which every key is optional: `{"version":1,"evidence":"required|standard","compliance":["gdpr",…],"tracker":{"provider":"github|jira|linear","site":"https://…","key":"ACME"},"reviewPublication":"off|auto|full","features":{"memory":false,"learning":false,"knowledge":false}}`. devflow never writes it. Each key is read on its own, so one bad value never disables the rest: a malformed or duplicated `evidence` resolves to `required`, a malformed `compliance` list to the generic lens, a malformed tracker value is reported rather than guessed, and unknown keys are ignored.
14
+ - **Evidence policy.** The evidence resolver takes the policy from the `evidence` key of `project.json` on the default branch. The working tree's copy can only raise it; offline, the default branch's local tracking copy stands in for the remote one (see **Fixed**). `HEAD` is not a source: it only feeds the advisory `pr-changes-policy` warning. `.devflow/policy.json` is retired in the same release (see **Changed**). A `compliance` key in `project.json` — on the default branch, its tracking copy or the working tree, whatever its value — raises the evidence floor to `required` even on a machine with compliance off.
15
+ - **Adopting it.** Commit `.devflow/project.json` on the default branch. The two halves of the file are read from different places: `evidence` always comes from the default branch, so a feature branch cannot lower the bar it is judged by, while every other key — `tracker`, `site`, `key`, `features` and the `reviewPublication` ceiling — comes from the checked-out branch, so a branch that edits them sees the change at once. `compliance` is the union of both: the default branch's frameworks, the checked-out branch's and your machine's, so a branch can add a framework but never remove one. The lens reads the default branch's copy from the local tracking ref, as of your last fetch, never over the network, so a framework added on the default branch applies once your checkout has fetched it. Keep a committed `.devflow/policy.json` until every teammate runs 3.0 (see **Changed**).
16
+ - **Settings, resolved locally.** A new `~/.devflow/scripts/resolve-settings.cjs` folds `project.json`, your personal `.devflow/config.json` and the machine manifest into one line (tracker, site, key, review publication, compliance lens, memory, learning, knowledge). It makes only local `git` calls and never touches the network. The repository selects its tracker provider, and your personal override may only narrow it to `github` or to that provider. A team `reviewPublication` is only a ceiling: it can lower your own setting, never raise it, so a branch that commits `full` still resolves `auto` for anyone who has not set `full` in their own `config.json`. Memory, learning and knowledge are on only when the machine switch is on and neither file sets that feature to `false`: a repository can switch a feature off for itself, never back on.
17
+ - **CLI.** `devflow memory|learning|knowledge --status` add an `Effective here: disabled (<file>)` line when a repository file narrows the feature — for knowledge, `disabled (<file> is unreadable)` when that file cannot be read, since an unreadable file switches knowledge write-back off — and print exactly what they did before otherwise. `devflow compliance --status` lists the frameworks this checkout's `project.json` declares (`Repository:`), those the default branch's copy declares (`Default branch:`) and the lens they add up to with your machine's (`Effective here:`), and while a retired `.devflow/policy.json` is in the working tree it says the file holds the repository at `required` and prints the `project.json` line that replaces it. `devflow compliance --enable`/`--set` now suggest the keys to add to `.devflow/project.json` (the required evidence setting plus your frameworks), merged into a file the repository already has rather than replacing it, instead of a `policy.json`, and still write nothing. `devflow tracker|memory|learning|knowledge --status` warn when this checkout's `.devflow/config.json` is tracked by git (see **Changed**) and print the `git rm --cached` line that fixes it.
18
+ - **Hooks honour the narrowing.** Memory and learning capture, the memory worker and the learning directive now stop in a checkout whose `project.json` or personal `config.json` sets `"features":{"memory":false}` or `"features":{"learning":false}`, and nowhere else. Both files are read at the checkout's root, even in a linked worktree whose learning ledger lives in the main checkout, and a `config.json` that git tracks is ignored here as everywhere. An unreadable file narrows nothing, so capture keeps running as the machine switch says. The old top-level `memory`/`learning`/`decisions` keys still switch nothing. A missing file, or one with no `false` in it, costs the hooks no extra process; a file that could narrow costs one `node` start.
19
+ - **Commands take the settings from that line, never from the files.** `/code-review`, `/resolve`, `/implement` and `/dynamic-build` take `reviewPublication` from it, so a team ceiling in `project.json` now applies to PR comments; with no `project.json` the value is exactly what your `config.json` said before, and a line that cannot be resolved publishes nothing (`off`, still the counts-only stub under a `required` evidence policy). Knowledge write-back in `/explore`, `/debug`, `/self-review`, `/implement` and `/resolve` now honours `features.knowledge: false` in `project.json` or your `config.json`, and skips when the line cannot be resolved. `/code-review` and `/plan` also learn the repository's compliance frameworks, and the review lens loads only those (see below).
20
+ - **`.gitignore` shares `project.json`.** The `.devflow/` block devflow maintains now carries `!.devflow/project.json`, just before its `.claudeignore` line, so the team file can be committed without `git add -f`. An existing block gains that one line the next time a hook or `devflow init` runs, inserted inside the block where a fresh block holds it — never at the end of the file, where it would override a `.devflow/project.json` re-ignore of your own. The block's comment now calls `policy.json` retired, kept for its presence only; nothing else in the block changes.
21
+ - **A repository selects its own issue tracker, and every install carries every provider** ([#393](https://github.com/dean0x/devflow/issues/393)). A committed `.devflow/project.json` with `"tracker":{"provider":"jira","site":"https://acme.atlassian.net","key":"ACME"}` now decides the tracker for that repository on every teammate's machine, trusted automatically, so one machine can work on a Jira repository and a GitHub one side by side. `devflow init --tracker` and `devflow tracker --set` set only the machine default that applies where no repository chooses. To make that possible, every install carries every provider's mechanics — all 47 generated references, the tool-call contract and the background Tracker agent — whatever the provider.
22
+ - **The Git agent reads one settings line.** It runs `resolve-settings.cjs` once per spawn and takes the provider, the Jira/Linear site and the project key from it; it no longer reads `.devflow/config.json` or the manifest itself. A personal `config.json` `tracker` override may only narrow the provider, to `github` or to the one the repository selects; one that names any other provider reports `tracker configuration mismatch (repository override)` and makes no tracker call, and the remedy is to correct or drop that key in your `config.json`; an unresolvable line reports `unknown tracker provider`. The site and key from `project.json` come before the learned conventions file.
23
+ - **Conventions are learned per provider**, into `~/.devflow/tracker/{provider}.md`, from the first repository that uses that provider. The background agent runs when the session's provider — the repository's, else the machine's — is not GitHub; the session-start check still costs no extra process on a GitHub machine, and one `node` start only when the repository's `project.json` or your personal `config.json` mentions `"tracker"`, so a personal narrowing to `github` also stops the background agent on a Jira or Linear machine. Inference attempts are counted per provider, so one unreachable tracker cannot use up another's five tries.
24
+ - **`devflow tracker --status`** adds an `Effective: jira (project)` line when the current repository selects a provider, and its `File:` line names the effective provider's conventions file, `~/.devflow/tracker/{provider}.md`. On GitHub it prints `Conventions: none (GitHub needs no learned conventions)` and `File: none`.
25
+ - **Compliance review follows the repository** ([#393](https://github.com/dean0x/devflow/issues/393)). A repository that declares `"compliance":["hipaa"]` in `.devflow/project.json` now gets a HIPAA review from `/code-review` and `/plan` on any machine, including one with compliance off. The lens takes its frameworks from the settings line: the union of your machine's, those the repository's `project.json` declares on its default branch (read from the local tracking copy, never the network) and those the checked-out branch declares. A branch can add a framework and never remove one, so a PR that deletes `"compliance":["hipaa"]` is still reviewed under HIPAA, and a repository file that cannot be read never lowers the lens. The reviewing agent loads the reference file for exactly those frameworks and no others. `/implement` and `/resolve` hand the same frameworks to their Code agents. Your machine's always-on compliance rule is never installed or changed by a repository.
26
+ - **`devflow init` warns before an older CLI downgrades the install** ([#406](https://github.com/dean0x/devflow/issues/406)). Running an older `npx devflow-kit@x init` over a newer install removed every skill, agent and command only the newer version ships, without a word. It still installs, but first says which version installed the machine and how to keep the newer assets (run `npx devflow-kit@latest init` instead).
27
+
28
+ ### Changed
29
+
30
+ - **BREAKING — `.devflow/policy.json` is no longer read; the evidence policy lives only in `.devflow/project.json`** ([#394](https://github.com/dean0x/devflow/issues/394)). `project.json` (added in this same release, #392) is the one place the team states its evidence policy, so no release honours both files. devflow never parses `policy.json` now: where `project.json` has no `evidence` key, a committed `policy.json` — whatever it says, `standard` included — resolves the policy to `required` with the `invalid-file` warning, and a repository with neither file resolves as before. **Migration:** add the value to `.devflow/project.json` — `{"version":1,"evidencePolicy":"standard"}` becomes `{"version":1,"evidence":"standard"}` (and `"required"` likewise); add `"evidence"` to an existing `project.json` rather than replacing it. Keep `policy.json` until every teammate runs devflow 3.0 or later, then delete it: 3.0 never looks at it once `project.json` has `evidence`, while an older devflow reads only `policy.json`, so deleting it early drops those teammates to the default policy. `devflow compliance --status` prints the line to commit while the old file is there. The `.gitignore` block still carries `!.devflow/policy.json` and `uninstall` still keeps the file, so a team's committed copy is never dropped. `/implement`'s stop remedy for a team that wants the `standard` policy now names `project.json`.
31
+ - **BREAKING — one install location per machine: `--scope local` and `DEVFLOW_DIR` are gone, `CLAUDE_CONFIG_DIR` is honoured** ([#389](https://github.com/dean0x/devflow/issues/389)). Before: `devflow init --scope local` wrote `<repo>/.claude` and `<repo>/.devflow` while every hook and prompt read `~/.devflow`, so a project-local install never worked; `DEVFLOW_DIR` was honoured by the CLI and some hooks but ignored by others, so an exported value split an install from the code that read it; and devflow's own `CLAUDE_CODE_DIR` named a directory Claude Code never reads. After: `~/.devflow` is always the machine root — the CLI, the HUD, every hook, every prompt and the evidence resolver resolve `$HOME/.devflow` and nothing else — and the Claude Code directory is `$CLAUDE_CONFIG_DIR` when it is an absolute path, else `~/.claude`, the same directory Claude Code itself uses. `init --scope` is hidden: `--scope user` is exactly the no-flag install, and `--scope local` exits 1 before writing anything. The manifest keeps its `scope` field for downgrade safety, always writes `user`, and reads a legacy `local` or missing value with every feature intact.
32
+ - **Migration.** If you run Claude Code with `CLAUDE_CONFIG_DIR` set, devflow now installs into that directory instead of `~/.claude`, and the install an earlier release left in `~/.claude` stays there, still loading for any Claude Code run without the variable. Remove it first: run `devflow uninstall` with `CLAUDE_CONFIG_DIR` unset, then `devflow init` with it set. If you set devflow's own `CLAUDE_CODE_DIR`, it is now ignored: remove it from your shell profile and set `CLAUDE_CONFIG_DIR` instead if you meant to move Claude Code's directory. An install an earlier release wrote to a `CLAUDE_CODE_DIR` directory stays there — remove it with `CLAUDE_CONFIG_DIR=<that directory> devflow uninstall`, then run `devflow init`. If you exported `DEVFLOW_DIR`, it is now ignored — re-run `devflow init` so the install lands in `~/.devflow`, then remove the old directory yourself. If a repo still carries a project-local install, run `devflow uninstall --scope local` from inside it: it removes that repo's `.claude` and `.devflow` install artifacts, keeps its project data, and never touches your home directory or your own `settings.json`. Outside a git repository, or in a repository rooted at your home directory (a dotfiles repo, whose `.claude` and `.devflow` are the machine-wide install), `uninstall --scope local` now exits 1 with "No legacy project-local install here" and removes nothing. A session started in a repository that still holds such an install says so once, pointing at `devflow uninstall --scope local`. `/code-review`'s language focuses and `/dynamic-profile` find Claude Code's directory by the same rule, so with `CLAUDE_CONFIG_DIR` set the language focuses run and the profile reads that directory's transcripts.
33
+ - **BREAKING — a `.devflow/config.json` that git tracks is ignored** ([#406](https://github.com/dean0x/devflow/issues/406)). The file holds your personal settings, and it is the one layer that may ask for `reviewPublication: "full"`, so a copy committed with `git add -f` let a contributor's branch lift a reviewer's local run past the team's publication ceiling. devflow now checks locally whether git tracks the file (`git ls-files --error-unmatch`, which reads the index without refreshing it); a tracked copy is read as absent, and the resolver says so on stderr. When git cannot answer, the file's keys fail closed. **Migration:** if your team committed `config.json` to share a tracker or a feature switch, move those keys into `.devflow/project.json`, then run `git rm --cached .devflow/config.json`; your local copy stays and becomes personal again.
34
+ - **The compliance skill and all six framework references are installed on every machine** ([#393](https://github.com/dean0x/devflow/issues/393)), so a repository can turn the review on by itself. The machine switch now owns only the always-on rule (installed and stamped with your frameworks when compliance is on) and the stamp on the skill, which names your frameworks or, with compliance off, none. `devflow compliance --disable` removes the rule and keeps the skill. `devflow compliance --status` no longer reports "artifact drift" from which reference files are installed — they all are — and still names any framework id in the manifest it does not know.
35
+ - **Tracker conventions move to one file per provider** ([#393](https://github.com/dean0x/devflow/issues/393)). The next `devflow init` moves `~/.devflow/tracker.md` to `~/.devflow/tracker/{provider}.md`, by the provider its frontmatter names, once and without overwriting anything; a file that names no provider stays where it is, with a warning. A provider change no longer moves conventions aside as `tracker.md.{previous}.bak`; existing `.bak` files are left alone as your content. `devflow tracker --status` on GitHub now counts 47 installed mechanics files, where it counted 24.
36
+
37
+ ### Fixed
38
+
39
+ - **Memory and learning now stop outside git repositories** ([#390](https://github.com/dean0x/devflow/issues/390)). Before: starting Claude Code in `~`, Downloads or a folder of repos made the hooks create `.devflow/` and a `.gitignore` block there, even with every feature off, and a launch from `~` put project data inside `~/.devflow`. After: no hook creates `.devflow/` or edits `.gitignore` outside a git repository or in a repository rooted at your home directory (a dotfiles repo), and memory and learning capture stop there. The home-directory check compares real paths, so a symlinked home and macOS's `/var` → `/private/var` are recognised. Directories polluted by earlier versions are left as they are; delete their `.devflow/` and the devflow block in `.gitignore` by hand if you want them gone.
40
+ - **Linked worktrees share the main checkout's learning ledger** ([#390](https://github.com/dean0x/devflow/issues/390)). Before: a `git worktree add` or `claude --worktree` checkout kept its own ledger, which restarted at ADR-001, collided with main's numbers and hid main's decisions. After: in a linked worktree whose main checkout already has `.devflow/`, learning turns queue into the main checkout's ledger, the Learning agent works there, and commands read decisions from it. Working memory and feature knowledge stay per checkout. A worktree ledger that already exists is kept on disk; new turns go to the main one.
41
+ - **Learning is no longer silently paused in `feat+x` worktrees, and says so when it is paused** ([#390](https://github.com/dean0x/devflow/issues/390)). Before: a project path containing `+` (Claude Code's name for a `feat/x` worktree), a space or other punctuation suppressed the Learning agent without a word, and the queue filled up and was never processed. After: `+` is accepted, and any other path the directive cannot safely carry produces a fixed `--- LEARNING PAUSED ---` notice, which names no path and asks the model to tell you once.
42
+ - **Decisions and feature knowledge load from the repository root when a session starts in a subdirectory** ([#390](https://github.com/dean0x/devflow/issues/390)). Before: commands read `.devflow/` relative to the current directory, so `claude` started in `packages/app` loaded no decisions or knowledge, and a knowledge write-back created and committed `packages/app/.devflow/features`. After: decisions come from the main checkout's ledger (else the toplevel), and knowledge bases from the checkout's toplevel, each resolved with one git call.
43
+ - **Detached HEAD has defined behaviour** ([#390](https://github.com/dean0x/devflow/issues/390), [#382](https://github.com/dean0x/devflow/issues/382) P02). A knowledge write-back still does not commit on a detached HEAD, but the workflow now tells you which files it left uncommitted. The first compaction on a detached HEAD bootstraps working memory stamped `branch: (detached)`, and the session header reads `detached @ <short-sha>` instead of `on unknown`. Detached worktrees stay out of `/code-review` and `/resolve` auto-discovery, and your personal `.devflow/config.json` stays per worktree; both are now documented.
44
+
45
+ - **devflow no longer takes over your own status line** ([#391](https://github.com/dean0x/devflow/issues/391)). Any `statusLine` whose command contained `statusline.sh`, `hud.sh` or a `/devflow/` directory was treated as devflow's, so `init` replaced it, and `init --no-hud`, `hud --disable` and `uninstall` deleted it. That included `~/.claude/statusline.sh`, the Claude Code docs' own example. A `statusLine` is now devflow's only when its command ends in `/.devflow/scripts/hud.sh` or the older `/.devflow/scripts/statusline.sh`; every other one is left alone.
46
+ - **`ambient --enable` works when you have your own Stop hook** ([#391](https://github.com/dean0x/devflow/issues/391)). It worked out the devflow directory from the first Stop hook in `settings.json`, so with a Stop hook of your own listed first (a notification sound, say) it registered a hook path that did not exist and every prompt failed. It now always registers the hooks under `~/.devflow`, where `init` installs them, and re-points any ambient hook devflow registered under another directory. If you hit this, run `devflow ambient --enable` again.
47
+ - **Your own hooks survive `init`, the feature toggles and `uninstall`** ([#391](https://github.com/dean0x/devflow/issues/391)). Any hook whose command merely contained the name of a devflow hook — `preamble`, `capture-turn`, `memory-worker`, `session-start-context`, `ensure-proxy` and the rest — was treated as devflow's: `init`, `ambient`/`memory`/`proxy --enable`/`--disable` and `uninstall` deleted it along with every other hook in its matcher group, and `--enable` skipped registering devflow's real hook because it took yours for it. A hook is now devflow's only when its command ends in `/scripts/hooks/run-hook <name>`, or in a form an older release wrote (`/scripts/hooks/ambient-prompt.sh`, the v1 `/scripts/hooks/{stop-update-memory,session-start-memory,pre-compact-memory}.sh`), and removing one leaves the rest of its matcher group in place, in order.
48
+ - **`uninstall` keeps the project files your team shares, and never offers to delete `~/.devflow`** ([#391](https://github.com/dean0x/devflow/issues/391)). The prompt said it would remove "docs, memory, learning" from `.devflow/` in the current directory, then deleted the whole directory, including the git-tracked `features/`, `conventions.md` and `policy.json`, so uncommitted edits to them were lost. Run from a repository subdirectory it missed the repository's `.devflow/`; run from your home directory it offered `~/.devflow`, the install itself. It now acts on the `.devflow/` at the git root, lists what it will remove and what it keeps, and always keeps `features/`, `conventions.md`, `policy.json` and `project.json`. Outside a git repository, or in a repository rooted at your home directory, it skips the step. When the repository's `.devflow` is a symbolic link you made, a confirmed cleanup used to delete it; it now leaves both the link and its target alone and says why, and `--dry-run` says the same.
49
+ - **The deny list now actually blocks a downloaded script piped into a shell** ([#399](https://github.com/dean0x/devflow/issues/399)) — the nine piped entries devflow shipped through v2.5.0 (`curl` or `wget` piped to `bash` or `sh`, `fetch | sh`, `lynx -source | bash`, and three `base64` decodes piped to a shell) never matched anything. Claude Code splits a Bash command at `|`, `&&`, `||`, `;`, `|&`, `&` and newlines and checks every deny rule against each piece on its own ([permissions docs](https://code.claude.com/docs/en/permissions.md#compound-commands)), so a rule containing ` | ` has no piece it can match. They are replaced by nine exact denies for a shell reading its script from standard input: `bash`, `sh` or `zsh` on its own, with `-`, or with `-s` (so `curl … | bash -s -- --yes` is blocked too). These match the shell half of the pipeline. `bash script.sh`, `sh ./x.sh` and other shells run on a named file stay allowed; `bash -c` and `sh -c` were already denied by their own entries, and `zsh -c` now is too. Like every Bash rule, these match the command as written: `/bin/bash` invoked by path is not covered. Re-running `devflow init` or `devflow security --enable` drops the retired entries from your deny list. `devflow security --disable` and `devflow uninstall` now remove them from managed settings too; that removal used to key on the current template, so it would have left retired entries behind.
50
+ - **Running `devflow init` again with the same options no longer changes anything** ([#388](https://github.com/dean0x/devflow/issues/388)). A second run used to leave three differences: `~/.devflow/scripts/package.json` became executable, because the scripts step made the whole scripts directory executable rather than just the scripts it copied. `~/.claude/settings.json` had `permissions` moved above the Claude Code flag keys, because re-applying the flags pushed each flag key to the end; they now keep their places, and `devflow flags` benefits too. And a first non-interactive `init --no-ambient` recorded `devflow-ambient` as installed, which the next `init` then dropped. The ambient plugin is now installed only when ambient mode is on, first run included, as it already was on every other path.
51
+ - **A shadowed compliance skill follows `devflow compliance --set`** ([#393](https://github.com/dean0x/devflow/issues/393)). `devflow skills shadow compliance` copied the installed skill, whose framework sections were already filled in, so the shadow carried no placeholders and every later `--set` left the old frameworks in place. The compliance shadow is now seeded from the shipped template, as the compliance rule's shadow already was, and a later `--set` stamps the new frameworks.
52
+ - **An unreachable remote no longer lets a branch lower the evidence policy** ([#406](https://github.com/dean0x/devflow/issues/406)). When `gh` and `git ls-remote` both failed, the resolver could not name the default branch, so the working tree's own `project.json` governed and a branch that set `"evidence":"standard"` resolved `standard` while the default branch said `required`. It now reads the default branch's name from the clone's local `refs/remotes/origin/HEAD` and folds that branch's tracking copy in, as the offline path already did when `ls-remote` answered. A clone that never recorded `origin/HEAD` keeps the old behaviour — the working tree governs, flagged `remote-unavailable` — while an `origin/HEAD` whose branch name is unsafe or cannot be parsed resolves `required`, as does a git that does not answer.
53
+ - **`devflow init` writes nothing into your home directory when it is a git repository** ([#406](https://github.com/dean0x/devflow/issues/406)). In a dotfiles repository rooted at `~`, init treated the home directory as a project: it wrote `~/.devflow/config.json` into the machine root, a `~/.claudeignore`, and devflow's block into `~/.gitignore`. It now writes no per-repository file there, runs no per-project migration, and says why, the same rule the hooks already follow.
54
+ - **`devflow init` never rewrites a `.devflow/config.json` it cannot parse** ([#406](https://github.com/dean0x/devflow/issues/406)). A file with a syntax error read as empty, and the rewrite deleted every key you had written by hand, your tracker override among them; a duplicated key read as its last value where the resolvers read the file as malformed. init now judges the file with the resolvers' own strict parser and, when it is malformed or unreadable, leaves it byte for byte as it was and prints a warning naming why. A `config.json` that is a symbolic link is treated the same way: devflow reads it as unreadable, so none of its keys apply and settings fail closed, as for any unreadable `config.json`, and init leaves it untouched with a warning.
55
+ - **Hooks no longer create `.devflow/` in a subdirectory of a checkout** ([#406](https://github.com/dean0x/devflow/issues/406)). When `git rev-parse` failed from a subdirectory — a repository git calls of dubious ownership, or one above `GIT_CEILING_DIRECTORIES` — the hooks still saw the checkout's `.git` further up and scaffolded `.devflow/` and a `.gitignore` block in the subdirectory. A directory now counts as a project only when the `.git` entry is in it.
56
+ - **Plans, research, reviews and evidence files land at the repository root from any subdirectory** ([#406](https://github.com/dean0x/devflow/issues/406)). About thirty `.devflow/docs/` paths in `/plan`, `/research`, `/bug-analysis`, `/implement` (handoff and evidence files) and `/dynamic-plan`, `/dynamic-tickets` and `/dynamic-build` were relative to the session's directory, so a run started in `packages/app` scattered a second `.devflow/docs/` tree there that no later run found. Every one now resolves from the checkout's toplevel; file names are unchanged.
57
+ - **Working memory no longer loses a turn's prompt** ([#406](https://github.com/dean0x/devflow/issues/406)). Claude Code runs a Stop event's hooks in parallel, so the memory worker could read the queue after the prompt was appended and before the response was, find only your prompt, and delete the queue. It now leaves that queue for its next run, which takes the whole turn.
58
+ - **`~/.devflow/logs` stops growing** ([#406](https://github.com/dean0x/devflow/issues/406)). Hooks log into one folder per working directory and nothing removed them; one machine held 31,000. `devflow init` now keeps the 200 most recently written folders and removes the rest in one run — a 31,000-folder backlog in about seven seconds, with only a backlog beyond 100,000 folders left for the next init — and a hook that creates a new folder — the debug trace included — trims the oldest beyond 200, at most 50 at a time. Files at the top of the logs folder, such as `proxy.log`, and symbolic links are never removed.
59
+ - **`settings.json` writes are atomic and keep a symbolic link** ([#406](https://github.com/dean0x/devflow/issues/406)). `init`, `debug` and `uninstall` wrote `~/.claude/settings.json` in place while the other commands renamed a temporary file over it, which replaced a dotfiles-managed symbolic link with a plain file. Every write now renames a temporary file into place and, for a linked `settings.json`, writes the file the link points to. `devflow debug --enable` with `"env": []` in `settings.json` reported success and wrote nothing; it now exits 1 and leaves the file untouched.
60
+ - **Help and hints state what the switches do** ([#406](https://github.com/dean0x/devflow/issues/406)). `init --no-compliance` said it removed the compliance artifacts; it removes only the rule, and the skill and framework references stay installed. `memory|learning|knowledge --enable` say a repository can opt out. The `docs-framework` skill no longer sources a `docs-helpers.sh` that no install provides.
61
+ - **devflow never runs a repository's `core.fsmonitor` command** ([#409](https://github.com/dean0x/devflow/pull/409)). Git runs whatever command a repository's `core.fsmonitor` names on every index read — `ls-files`, `status`, `diff` — so a repository shipped with a crafted `.git/config` (a tarball, a zip, a shared drive) could run code as soon as a devflow hook or the HUD read its index. Every index read devflow ships now passes `-c core.fsmonitor=false`. The HUD keeps git's own built-in monitor (`core.fsmonitor=true`) for its status and diff reads, so large repositories stay fast; a hook path, an unset value or a config it cannot read keeps the override.
62
+ - **The HUD no longer shows a lone unstaged edit as staged** ([#409](https://github.com/dean0x/devflow/pull/409)). Trimming `git status --porcelain` removed the leading space of its first line, so a single unstaged change to a tracked file read as staged and the tree as clean. Only trailing whitespace is trimmed now.
63
+
64
+ ### Tests
65
+
66
+ - **The unit suite can no longer write to your real install** ([#388](https://github.com/dean0x/devflow/issues/388)) — every test file now runs under its own temp `HOME` with `DEVFLOW_DIR`, `CLAUDE_CODE_DIR` and `CLAUDE_CONFIG_DIR` unset (`tests/setup/isolate-env.ts`), and it fails loudly if that `HOME` is ever the real one. Spawned CLIs and hooks get their env from a shared `sandboxEnv(home)` allowlist rather than a copy of `process.env`. A canary test proves that an exported `CLAUDE_CONFIG_DIR` / `DEVFLOW_DIR` stays empty while `init` runs, and a red probe proves that `CLAUDE_CONFIG_DIR` really does redirect writes when left in place, while `DEVFLOW_DIR` does not: devflow ignores it.
67
+ - **The memory-worker gate tests no longer fail on a loaded machine** ([#390](https://github.com/dean0x/devflow/issues/390)) — two `capture-hooks` tests checked a marker the stand-in `claude` touches, which the 2-second test watchdog could kill before it ran, and they ran inside the default 5-second test timeout, which a loaded machine exceeded. They now check the worker's own log, written before the test's synchronous run returns, under a timeout derived from the worker's watchdog and kill grace.
68
+ - **CI runs the shell-hook suites under macOS `/bin/bash` 3.2** — a new `macos-bash32-hooks` job puts `/bin/bash` first on `PATH` and refuses to run unless `bash --version` reports 3.2, so a bash-4-only construct in a hook fails in CI instead of on a Mac.
69
+ - **No prompt reads `.devflow/project.json` or `.devflow/config.json` itself** ([#392](https://github.com/dean0x/devflow/issues/392)) — `tests/guards/no-config-read.test.ts` scans every installed prompt (compiled commands, agents, the git references, skills and rules) for a read verb, a reading command or an input redirect aimed at either file, with red probes for each shape and negative controls for mentions that are not reads. There is no exemption: the Git agent takes its tracker settings from the settings line like every other prompt.
70
+ - **The retired policy file stays unparsed** ([#394](https://github.com/dean0x/devflow/issues/394)) — `tests/evidence-policy/resolver.test.ts` resolves a `policy.json` saying `standard`, `required` and invalid bytes to `required` at every source, proves the working-tree file is never opened (a FIFO in its place, which blocks any reader, never stalls the resolver), pins that a migrated `project.json` resolves the same with a `policy.json` beside it (which is never probed), and fails on any `parsePolicyBytes`, `POLICY_GRAMMAR_RE` or `serializePolicy` under `src/`, with a red probe.
71
+ - **A guard keeps the retired variables out** ([#389](https://github.com/dean0x/devflow/issues/389)) — `tests/guards/one-home.test.ts` fails on any whole-word `DEVFLOW_DIR` under `src/` or `dist/` other than the settings template's install-time `${DEVFLOW_DIR}` token (at the template and its one substitution site) and on any `CLAUDE_CODE_DIR`, with a seeded probe per spelling it claims to catch.
72
+ - **Install snapshots and an installed-hook matrix pin what devflow writes** ([#388](https://github.com/dean0x/devflow/issues/388)) — `tests/install-snapshot.test.ts` installs the built CLI into temp sandboxes for three configs (`--recommended`; `--tracker jira --compliance hipaa`; every `--no-*` switch) and compares normalised goldens: the HOME and repo walks with exec bits, `settings.json`, the manifest, what a second `init` changes, and what a non-interactive `uninstall` leaves behind. Every installed hook command also runs via `sh -c` in a repo root, a subdirectory, a linked worktree and a non-git directory, one test per cell, against `hook-matrix.txt`. A second `init` with the same argv must change nothing the snapshot records. The goldens record the hooks' real behaviour cell by cell, and a fix that changes a cell regenerates them in a fixture-only commit. Regenerate with `npm run test:golden:update -- install-snapshot`.
73
+ - **The compliance lens is pinned from the settings line to the reference an agent loads** ([#393](https://github.com/dean0x/devflow/issues/393)) — `tests/compliance-prompts.test.ts` checks that `/code-review` and `/plan` gate the lens on the settings line and pass the framework ids to the compliance Review and Design agents, that `/implement` and `/resolve` pass them to every implementing Code agent, and that no prompt, agent or skill lets installed files choose the frameworks, with a probe for each retired wording. `tests/compliance-e2e.test.ts` installs into a scratch home, declares `hipaa` in a scratch repository and checks that the installed resolver reports `COMPLIANCE=hipaa` while the machine's compliance rule stays absent on a compliance-off machine and byte-identical on a `gdpr` one.
74
+ - **Every CLI toggle's writes are fenced, and `init` is checked from every location** ([#406](https://github.com/dean0x/devflow/issues/406)) — `tests/write-set-fence.test.ts` runs every `--enable`, `--disable` and `--set` each command defines, plus the clearing and read-only actions beside them, against one sandbox installed by the built CLI, and fails on any path a run writes outside that action's declared set; a command or option with no row and no stated reason fails too. `tests/init-location-invariance.test.ts` runs `init` from a repository root, a subdirectory, a linked worktree, a non-git directory, a repository path with a space, a symlinked home and a home that is itself a repository, and requires the same machine install from each, with per-repository files only at the checkout's toplevel. Both counts are ratchet floors.
75
+ - **New guards** ([#406](https://github.com/dean0x/devflow/issues/406)) — `tests/guards/claude-dir.test.ts` fails on a prompt that reads a path under the home `.claude` directory instead of resolving `CLAUDE_CONFIG_DIR`; `tests/guards/docs-root.test.ts` fails on a relative `.devflow/docs` path in a compiled command; `tests/guards/settings-atomic-write.test.ts` holds every `settings.json` write to the one atomic helper; and `tests/guards/no-config-read.test.ts` now also scans the ambient charter and the directives hooks emit, and fails when `dist/` is older than its sources. Each has red probes.
76
+ - **A guard keeps shipped git index reads off `core.fsmonitor`** ([#409](https://github.com/dean0x/devflow/pull/409)) — `tests/guards/no-fsmonitor-index-read.test.ts` fails on any shipped `ls-files`, `status` or `diff` call without `-c core.fsmonitor=false`.
77
+
78
+ ---
79
+
80
+ ## [2.5.0] - 2026-09-27
81
+
82
+ ### Added
83
+
84
+ - **Pick your issue tracker: GitHub, Jira, or Linear** — before: every traceability path in devflow assumed GitHub. An issue reference was a bare `#123`, the only mechanics that existed spoke `gh`, and a team whose issues live in Jira or Linear had no way to say so; the provider-shaped hole the Phase-2 refactor opened had exactly one occupant. After: the provider is a selection — `manifest.features.tracker = { provider }` over `github | jira | linear`, defaulting to `github`, machine-wide rather than per-project (like `proxy` and `compliance`). Choose it at `devflow init`, non-interactively with `devflow init --tracker <id>`, or afterwards with `devflow tracker --set <id>`; inspect it with `devflow tracker --status`, which also reports whether conventions have been learned yet and where the file lives. The wizard asks only where the question can be answered: Advanced always, Recommended only when you actually reached the Setup-mode prompt interactively, and never on `--recommended` or a non-TTY run — so both promptless contracts are preserved, and passing `--tracker` suppresses the question on either path. There is no `--no-tracker`: `--tracker github` is the off switch, because a flag whose only meaning is "⇒ github" is a second spelling of a value that already exists. The ID is matched byte-exactly against the registry — `JIRA`, `jira ` and `jira-cloud` are **rejected with an error, never repaired** — so a typo cannot quietly select a tracker you did not name, and the validated token selects a hardcoded path prefix from a static map instead of ever being concatenated into a path. A malformed value already sitting in the manifest self-heals to `github` silently and is deliberately kept out of the manifest's hard-null set, so every pre-tracker install still reads as a prior install rather than as no install at all. Changing the provider moves any conventions file inferred for the old one aside as `tracker.md.{previous}.bak` and re-arms inference; `--reset` collapses the selection to `github` and still fires that rename, because the prior provider is a real transition even when the reset makes the new one the default. **Existing installs and every GitHub user see nothing change** — no prompt, no new file, no altered byte, and under provider `github` the new session-start gate performs **zero** subprocess invocations.
85
+
86
+ - **A background agent learns your tracker's conventions once, silently** — before: nothing in devflow knew a project key, an issue-type vocabulary, a required-field set or a workflow's transition names, and there was no place to put them. After: on a non-GitHub provider, Section 3 of the `session-start-context` hook emits a silent `--- TRACKER SETUP ---` directive that spawns the new **Tracker agent** (the 17th agent, `sonnet`) in the background. It is never narrated, never a question, and never spawned from a command — it has no workflow roster row at all. The agent claims `~/.devflow/.tracker.processing`, probes what the connected tracker can actually do **by capability description rather than by tool name** (published tool rosters disagree across vendors and versions, so a name-matched probe reports "missing" for a capability that is present under another spelling), infers repository conventions from the bounded history scan it loads out of the `devflow:git` skill rather than restating it, and writes `~/.devflow/tracker.md` **exactly once or not at all** — create-exclusive, mode `0600`, gated on the secret scrubber through a single `&&` chain so a scrub failure writes nothing, with a `# UNRESOLVED:` sentinel on every line it could not establish and a `## Dedup Strategy` section recording what the probe observed. A partial or defaults-only file would be worse than no file, because the file's existence is the signal that setup is done. The gate in front of it is cheapest-first and bounded in four independent ways: a zero-byte `.tracker.enabled` sentinel plus the absence of `tracker.md` (two shell builtins — the reason GitHub costs nothing), an attempt cap of five counted in `.tracker.attempts` and incremented when the directive is *emitted* rather than when the agent finishes (a crashed agent still burns an attempt), a `source` restricted to `startup` and `clear` so no agent is spawned into a session already mid-flight, a 600-second claim-file freshness check, and a **positive** `jira|linear` allowlist that runs before any interpolation. `devflow init` and `devflow tracker --set` reset the attempt counter; `devflow tracker --status` is read-only and resets nothing.
87
+
88
+ - **The Git agent resolves the tracker provider once per spawn, and refuses a stale configuration** — before: no single place resolved a provider, so a provider token would have had to be threaded through roughly thirty filename-composition sinks. After: the agent's preamble resolves it once, and **not** by taking the first rung that answers — rung 1 narrows rung 2 and cannot be evaluated without it, so both are read before anything is decided. (1) the `tracker` key in the project's `.devflow/config.json`, which **narrows only**: it admits `github` or the manifest's own provider and nothing else, and any other value is `TRACEABILITY: DEGRADED (tracker configuration mismatch (repository override))` with no tracker call. (2) `manifest.features.tracker.provider`. (3) `github`. The repository's own **issue-reference grammar** is not a rung: it narrows what is already resolved and never selects. The remote, the hosting platform and the PR host are *not* signals either — devflow itself keeps PR hosting on GitHub while a team's tracker is Jira, so a rule reading the remote would disable the feature for exactly the users it exists for. The deciding signal is named on the status line. And the reader half refuses rather than guesses: `tracker.md` whose frontmatter `provider:` disagrees with the resolved provider produces `TRACEABILITY: DEGRADED (tracker configuration mismatch)` and **no tracker call**, which covers every path init cannot see — an uninstall then reinstall, a hand edit, a dotfile-repo sync. On a non-GitHub provider a `- **Tracker**: {provider} ({winning source})` line joins `- **Conventions**:` in the Traceability block; **the GitHub path emits no tracker status line at all**, which the `tests/fixtures/golden/github-status-lines.txt` corpus is what asserts.
89
+
90
+ - **`redact-secrets.cjs --emit` — the D11 scrub gate for sinks with no shell boundary** — before: the scrub was expressed as a `&&` chain, which works for a file sink and cannot exist inside a tool call. At a tool-call sink the rule degraded to an instruction, and an instruction is not a gate. After: `--emit` scrubs its input **twice** and prints a framed result on stdout — `D11-OK <nonce> <sha256> <bytes> <n> [type:count,…]` on line 1, the scrubbed body from line 2 — where the second pass returning zero findings **is** the gate, and the nonce is 32 hex characters generated per invocation and required, because composed bodies carry untrusted issue text and an unframed `D11-OK` literal is forgeable by anyone who can write an issue comment. Every non-zero path prints `D11-FAIL <reason>` with an **empty body** — no path, no secret, no partial content — and the no-body property belongs to the result type rather than to a caller remembering to suppress it. A new exit code `5` distinguishes "the gate refused" from a usage error, an unreadable input or an internal fault. The consumer's obligation is mechanical too: compare the received body's byte length against `<bytes>` before posting, and on mismatch **do not post**. That check exists because a Bash result is clipped at a per-machine character limit with its middle elided, so the framing line and the body's tail both survive a truncation — and a bare "is the framing line there?" gate would pass over a body with a hole in it.
91
+
92
+ - **A provider-independent tool-call contract for tracker I/O** — `src/assets/mds/tracker/_mcp.mds` states, once, the rules every MCP-backed provider's mechanics must follow: a seventeen-row capability table mapping each capability to what happens when it is unreachable (only *identify current user* posts anyway, reporting that dedup was unavailable); no HTTP fallback of any kind — no `curl`, no `wget`, no credential read from the environment, no substituted CLI; scrub-before-render, where the only permitted wrapper is a pure structural one whose concatenated text equals the scrubbed bytes, so no re-encoding, chunking, summarising or reflowing can reintroduce what the scrub removed; structured reads whose **shape** is trusted and whose **values** are not; and a one-directional load chain in which this contract wins on any conflict. It names neither provider and not the transport acronym, stating its rules in terms of capabilities instead — which is the same capability-first doctrine it imposes on its readers, applied to its own prose. It is generated to `references/tracker/_mcp.md` only once a provider module that needs it is registered.
93
+
94
+ - **Jira tracker mechanics — the first provider to reach its tracker through tool calls** — before: the provider slot existed and had one occupant. A team on Jira could select `jira`, and every tracker operation then degraded with `tracker mechanics unavailable`, because no mechanics existed for it. After: eleven generated references under `references/tracker/jira/` — one per tracker operation, the same operation set GitHub has, read from one shared roster so a provider cannot silently acquire or lose an operation. Every call goes through a tool the session already exposes, selected **by capability description rather than by tool name**; there is no `curl`, no `wget`, no credential read and no substituted CLI anywhere in the tree, and a capability that is absent degrades with that capability named rather than being improvised around. Jira's own facts are stated and GitHub's are conspicuously absent: the body cap is `32767`, backpressure is `Retry-After` honoured verbatim with a STOP on 429 and **no pre-emptive rung at all** — Jira publishes no remaining-request count, so a threshold keyed on one would never fire and would read as coverage while providing none. A batch fetch is **one** filtered query (`key in (…)` with a result bound), never fifty sequential ones, and a guard forbids both the tool-name and the capability-name spellings of a per-item fetch inside that reference, because the second is what this project's own capability-first doctrine steers an author towards writing. Dedup climbs a four-rung ladder — an invisible entity property, then editing the existing comment in place, then a first-line marker filtered to comments this account authored, then posting with a warning that a duplicate is possible — and the marker is **the comment's first line and nothing else**: Jira comments have no HTML-comment node, so a marker at line 5 is somebody quoting you, and a substring search over the whole comment is precisely how a quoter would acquire the power to silence a release note. Markers are namespaced per comment kind (`devflow:shipped`, `devflow:wave`, `devflow:traceability`), each owned by exactly one operation, because one global marker would make the three kinds suppress each other. Where an operation checks markers inside a bounded loop the cost is stated as a **product** rather than a per-call bound — fifty issues by two pages is a hundred calls, reported as truncated past that — alongside the cheaper shape that avoids the loop entirely. A design artifact that GitHub posts as a collapsed `<details>` block becomes a **pointer sentence**, since Atlassian's document format has no collapsed-block analogue and no official converter; on truncation the marker and the status lines are what survive and the untrusted middle is what gets cut. Jira absent or denied still cuts the branch and opens the PR, records `Tracked (pending)` with the reason, and **never falls back to creating a GitHub issue** — a different tracker is not a degraded version of the one you chose. **GitHub users see nothing change:** the GitHub path's own per-spawn cost is unmoved, the tool-call contract is billed at zero there because no GitHub mechanics file loads it, and the frozen status-line fixture is still byte-identical to its original capture.
95
+
96
+ - **Linear tracker mechanics — shipped at rank 4, and honest about it** — before: `linear` was a selectable provider with no mechanics behind it, so every tracker operation degraded with `tracker mechanics unavailable`. After: eleven generated references under `references/tracker/linear/`, the same operation set the other two providers have, from the same shared roster. An issue reference is a team key (`TEAM-123`) **or** an internal id, each matched against its own separately anchored pattern after upper-casing — separately, because one pattern wrapping an alternation anchors the left half at the start and the right half at the end, and `1; id` walks through the middle of that. A batch fetch is **one** filtered query with a page bound, never fifty sequential ones. Backpressure is an HTTP **400 carrying `RATELIMITED`**, not a 429, and it arrives as error text rather than a status line — so the mechanics name the code, the token and the text form, because the generic rule for a 4xx is "degrade this item and continue", which would keep the fan-out running straight into the window the rule exists to stop. **Where Linear differs from its sibling is what devflow cannot do, and it says so instead of pretending otherwise.** On a stock Linear workspace devflow cannot ask the tracker which account it is, and the attachment upload takes bytes rather than a URL, so three of the four dedup rungs are unreachable and devflow lands on the fourth: it **posts a back-link with a warning** rather than suppressing one it cannot verify. Every run emits `TRACEABILITY: DEGRADED (dedup unavailable — duplicate possible)`, and each comment's first line carries the marker plus the devflow project URL — the line binding defeats somebody quoting your comment, the URL defeats a coincidence, and together they are the only evidence available when there is no author to filter on. A missing release back-link is worse than a second one you were told about. The one place the opposite rule wins is a wave report, where a scan that ran out of pages says the evidence exists and was not read, and a duplicate wave report leaves the next run unable to tell which is authoritative — so that one fails closed and posts nothing. A design artifact becomes a **pointer sentence**, as on Jira; Linear absent or denied still cuts the branch, opens the PR and records `Tracked (pending)` with the reason, and **never falls back to creating a GitHub issue**.
97
+
98
+ - **Linear's borrowed limits are written down rather than presented as measurements** — the comment-body cap devflow uses for Linear is `32767`, which is **Jira's documented cap adopted as the conservative choice**; Linear publishes no cap that any of this work measured. That, and the rank-4 dedup reality above, are recorded as `## Known Unknowns` in the Linear mechanics module and surfaced for users in `docs/cli-reference.md`, with [issue #343](https://github.com/dean0x/devflow/issues/343) as the owner and the artifact — it names the two files the borrowed values live in, so a measurement lands in one change instead of being hunted for. Borrowed too generously, a long post is rejected at the tracker and degrades with a reason; borrowed too strictly, a body that would have fit is truncated with a pointer to the local artifact. Nothing is lost silently either way.
99
+
100
+ - **The tracker operations no longer describe themselves as GitHub operations** — before: the Git agent's always-loaded operation table read "Fetch GitHub issue", "Fetch multiple GitHub issues" and "Create or enrich a GitHub issue", and its shipped-issue back-link required every issue reference to be **digits only**. The first was wrong for two of three providers; the second was a live defect — under Jira or Linear every `PROJ-1` was dropped by a gate that ran before any provider mechanics were consulted, which made the operation unreachable. After: the table and the two operation descriptions that duplicate it say "tracker issue", and the entry gate defers to the resolved provider's own anchored reference grammar, with GitHub's `#123` form moved into the GitHub mechanics beside the same drop rule. **The gate is relocated, not relaxed** — the operation always loads its provider's mechanics, and a missing mechanics file already means no tracker call at all. GitHub literals that are not tracker facts stayed put: releases and pull-request review threads name GitHub because both stay there whichever tracker you choose. The always-loaded prompt came out **25 characters shorter and one line shorter**, so every per-spawn budget gained headroom while gaining a provider.
101
+ - **Devflow installs only what your selection uses** (#350) — before: every skill from every plugin was installed regardless of which plugins you picked, and every tracker provider's mechanics shipped to every machine: a GitHub-only user carried Jira's and Linear's reference files, the tool-call contract and the Tracker agent, and a Go project carried the React skill. After: each plugin hand-declares in a `requires:` field the skills it uses but does not own, and the install set is the closure of `skills ∪ requires` over the plugins you selected — the default (non-optional) plugin set installs **32 of the 40** plugin-owned skills, the eight left out being exactly the language skills of the optional language plugins. A bidirectional closure guard holds every `requires` entry against what that plugin's commands, agents and skill bodies actually name, in both directions, so an entry cannot be added speculatively and a skill cannot be used without being declared; the single reference no literal can resolve — the Review agent's focus-templated one — is a classified exception rather than an unexplained gap. The tracker bundle is scoped the same way: the installer overlays the provider-independent generated references (the PR-host mechanics and the cross-cutting documents) plus the `{github} ∪ {selected provider}` tracker mechanics — **24 files for `github`, 36 for `jira`/`linear`**, nine of them the PR-host mechanics every install carries — and `references/tracker/_mcp.md` and the Tracker agent file land **only** for `jira`/`linear`. The build still compiles every provider, so the tarball is unchanged and switching providers needs no rebuild. Section 3 of `session-start-context` stays installed for everyone and is gated at runtime, because a hook removed from `settings.json` is a hook a re-install has to remember to put back.
102
+
103
+ - **An evidence policy the team commits: `.devflow/policy.json`** (#370, #371) — before: how much traceability a change needed depended on whether the compliance skill happened to be installed on the machine running devflow, so two teammates on one repository could hold the same change to different standards, and nothing a team committed could settle it. After: `EVIDENCE_POLICY` is `standard` or `required`, resolved once per run by `resolve-evidence-policy.cjs` from `.devflow/policy.json` as committed on the repository's **default branch** — `{"version":1,"evidencePolicy":"required"}`, exactly those two keys, at most 4 KiB. The stricter value always wins: local sources — enabled compliance, the worktree's own copy — can raise the policy and never lower it, a branch that commits a weaker file is still judged by the default branch's copy and shows a `pr-changes-policy` warning, and an invalid file or a resolver that cannot answer resolves to `required`. Offline, the local copies are read and the result is flagged `remote-unavailable`. With no committed file the policy is `standard`, or `required` on a machine with compliance enabled at any framework count, zero included. Operations never see the policy itself, only three inputs the resolver alone derives from it: `ISSUE_REQUIRED`, `APPLY_CONVENTIONS` and `REQUIRE_NON_AUTHOR_APPROVAL`. Under `required`, `/plan` creates or enriches a tracker issue; `/implement` without a linked ticket, or without a test plan, asks before any code is written and either records a self-attested exception under `## Evidence Exceptions` in the PR body or stops; naming conventions are learned once into `.devflow/conventions.md` and applied to branch names and PR titles; `/resolve` replies to external review threads, resolves the ones it verifiably fixed and checks merge readiness; `/release` gates on the trace map; `/dynamic-tickets` files issues; and `reviewPublication: off` still posts the counts-only stub. `/code-review` and `/bug-analysis` carry no ticket gate under either policy. `devflow compliance --status` prints the resolved policy and where it came from, and `--enable`/`--set` print a `policy.json` for you to commit — **the CLI never writes the file**.
104
+
105
+ - **Test-plan evidence on every pull request devflow opens** (#372, #375) — before: a PR said what was tested only in prose, with nothing but the writing agent's word behind it. After: `/plan` and `/dynamic-plan` write the test plan as TP lines — `- [ ] TP-1 (AC-1) {scenario} — method:ci [files: src/upload/**]` — where `method` is `ci`, `local` or `manual` and the scenario is plain words with no `#`, `@`, `/`, `<`, `>`, brackets or backticks, because it lands in a PR body where a closing keyword or a mention would act. `/implement` checks the lines before any code is written, writing them from the stated acceptance criteria when a plan has none, and the PR body carries them in a block between `<!-- devflow:test-plan -->` markers — on `/implement`, `/code-review`, `/bug-analysis` and wave PRs alike. The Test agent records a claim per line — `PASS`, `FAIL` or `SKIP` at a commit — and only the evidence scripts (`pr-evidence.cjs` and `verify-evidence.cjs`), never a prompt, turn a claim into one of six states — `VERIFIED-CI`, `ATTESTED-LOCAL`, `UNVERIFIED`, `STALE`, `FAILED` or `INDETERMINATE` — by checking commit ancestry, the diff since the claim and the CI runs at that exact commit. Only the first two count as verified, and the block ticks exactly those lines. A new Git operation, `update-pr-evidence`, re-derives every state at the PR's head, rewrites only the marked block and posts an append-only evidence comment keyed to that head: a counts-only **STUB** unless `reviewPublication` is `full`, which adds the scenario table with links to the CI runs and falls back to the STUB above 55,000 characters. `/implement`, `/resolve` — after re-verifying stale lines with one Test agent, when its run pushed — and the wave PR all refresh it. Merge readiness reads the same evidence: READY needs every line verified, or no lines and a recorded `test-plan` exception, and under `required` a trusted non-author approval as well. Who counts as trusted is one shared reference, `trust-rule.md`: a review thread carrying a devflow marker is hidden only when its author is the viewer, or an owner, member or collaborator with write or admin permission.
106
+
107
+ - **Release traceability: every commit since the last release, mapped to its issue** (#374) — before: `/release` found the last release with `git describe`, which a local marker tag could win, and nothing showed which shipped commits carried no issue reference. After: `release-trace.cjs`, an offline script, picks the highest merged `vX.Y.Z` or `X.Y.Z` tag — marker and prerelease tags never win — and sorts up to 500 first-parent commits since it into traced, exempt (a release, revert or bot commit) and untraced. `/release` takes the last release from it under either policy, and `--dry-run` shows the traced, untraced and exempt arms without asking anything or writing a checkpoint. Under `required`, untraced commits or unknown coverage get one question before the confirm — record self-attested exceptions, or halt — and the recorded exceptions, with every exempt commit, are appended to the release notes as `## Traceability exceptions`, which the notes size cap never drops. A new Git operation, `associate-release`, then adds each shipped issue to a GitHub milestone, a Jira fixVersion or a Linear label named `v{version}` — adding, never replacing, and never blocking the release. Jira and Linear evidence gathering always ends `PARTIAL`, which warns and does not stop.
108
+
109
+ - **Wave pull requests carry their tickets' evidence, and each ticket gets its own issue** (#375, #376) — before: a wave handed its tracking issue to every ticket, so a whole wave's work traced to one issue, and its tickets branched from `HEAD` rather than from the integration branch. After: under `required`, `/dynamic-tickets` files the tracking issue and then one issue per ticket, in dependency order, one at a time, at most 50, each issue's `**Depends on:**` line rewritten to the references its dependencies were filed as, and writes a shape-checked `**Issue:**` line into each ticket file. Each ticket is built against its own issue and branches from the integration branch, and `/dynamic-build` stops a ticket that has no issue while one is required. After an explicit "open" — a headless run counts as no — a wave opens one PR from `wave/<slug>` carrying a leading `Refs` line for the tracking issue (never `Closes`, and never a second one), a `Closes` or `Refs` line per ticket, a checked `## Wave Evidence` table and the test-plan block; under `required` it is blocked while a merged ticket lacks a usable test plan or an issue link. Once the PR is open, one Test agent claims the wave's TP lines on the integration branch and `update-pr-evidence` refreshes the block and posts the evidence comment; a failure there is reported as `TRACEABILITY: DEGRADED` and never blocks.
110
+
111
+ ### Changed
112
+
113
+ - **`devflow uninstall` can now ask before clearing `~/.devflow`, where a Jira or Linear user previously got a silent sweep** — this is the one accepted user-visible regression in the tracker work, and it follows from classifying `~/.devflow/tracker.md` as **your content** rather than as an install artifact. Before: a user-scope interactive uninstall for someone with no other user content in `~/.devflow` resolved to an artifacts-only sweep and removed the directory's devflow files without asking. After: a user who has selected Jira or Linear has a `tracker.md`, and a `userContent` entry flips that same interactive uninstall to a confirm prompt — so an inferred conventions file, which is hand-editable and represents real setup effort, is never deleted without a question. `.tracker.enabled`, `.tracker.attempts` and `.tracker.processing` remain install artifacts and are swept normally; the two lists stay disjoint. **GitHub users are unaffected**: no `tracker.md` is ever written for them, so the prompt cannot appear. The classification is deliberately conditional. The precedent it copies is the `agent-models.json` reclassification, where *"silently"* was the load-bearing word: stale per-agent overrides re-applied silently, so they were demoted to an install artifact. A stale `tracker.md` is safe to preserve only because the provider-mismatch guard removes the silence — a file whose frontmatter `provider:` disagrees with the resolved provider produces `TRACEABILITY: DEGRADED (tracker configuration mismatch)` and no tracker call. **Reversal condition, recorded:** if that guard is ever dropped, descoped or softened, `tracker.md` is reclassified back to an install artifact **in the same change**, because otherwise a silently-authoritative stale file survives an uninstall.
114
+
115
+ - **The Git agent's GitHub mechanics now live in generated skill references** — before: `git.md` was 65,677 characters re-sent on every Git spawn, roughly 9,400 of them GitHub-specific mechanics (`gh` invocations, header names, rate-limit thresholds) interleaved with the provider-independent contract — each operation's `**Input:**`, `**Output:**` template and `**Degradation (D4):**` clause. There was no single place a tracker provider was resolved, so a provider token would have had to be threaded through roughly thirty filename-composition sinks. After: this split alone brought the compiled agent to 58,100 characters — the PR-host split (below) cuts it further, under a single `BUDGET_GIT_MD` ceiling (the byte-budget entry carries the figures). A ≤36-line provider-resolution preamble resolves the provider **once per spawn** and states the **one** load instruction that composes a mechanics path; thirteen generated references carried what moved at the time — ten per-operation GitHub files under `references/tracker/github/`, plus `learn-conventions.md`, `publication-gate.md` and `decision-markers.md` (a GitHub install now carries 24 generated references once the PR-host set is counted; see the entry above). Every move is byte-identical unless it is one of 63 named, individually justified exemptions, and a containment oracle compares the pre-split tree against the post-split one line by line to prove it — over the whole branch diff, 150 of the 160 content lines the golden lost are byte-present elsewhere in the loadable set and the remaining 10 fall inside a named exemption range, with none unaccounted. Zero user-visible change: `Tracked = #{n}`, `Depends on: #{n}`, `42-jwt-auth.{ts}.md` and `issue: 42` all render exactly as before. This entry is an internal refactor — it adds no new prompt and no new file to any user's project tree.
116
+
117
+ - **The D4 and D11 cross-cutting contracts are provider-independent in fact, not only in claim** — before: the always-loaded degradation contract named `gh` as the thing that can be unauthenticated and stated GitHub's own rate-limit signals (a 403/429 body, `X-RateLimit-Remaining < 10`, the `< 50` backpressure rung) in the same sentences as the provider-independent STOP/THROTTLED rules; the comment-sink scrub said it applied to bodies posted "to GitHub". Two authorities on the redaction path. After: the invariants stay inline and unchanged — the scrub is still unconditional, still fail-closed, still `&&` and never a pipeline, and the scrubber invocation itself is never made loadable — while the cross-cutting blocks themselves name no provider. D11's shell recipe now posts through a `<the resolved provider's post command>` placeholder that the operation's own generated reference resolves. D4's GitHub-specific detail — the 403/429 rate-limit body, the `X-RateLimit-Remaining < 10` STOP threshold and the `< 50` backpressure rung — left the cross-cutting block entirely and is *explained* once, in the GitHub reference of `backlink-shipped-issues`, the operation that owns the fan-out. It is not *stated* only there, and deliberately so: `skills/git/SKILL.md` is preloaded on every Git spawn and keeps the `< 10` STOP threshold, which is the mitigation that makes the move safe, and each fan-out operation's own `**Degradation (D4):**` clause still names the signal it acts on inline beside the `THROTTLED` report it triggers — in the agent and in the generated references alike. A threshold an agent must recognise before it acts is worth restating at the point of use; a header name, a status code and a shell command are not.
118
+
119
+ - **`skills/git/SKILL.md` no longer contradicts the agent it is preloaded with** — before: 9,205 characters preloaded on every Git spawn, carrying two live safety contradictions — `if [ "$REMAINING" -lt 10 ]; then sleep 60; fi`, which tells the agent to wait out exactly the secondary rate limit D4 tells it to STOP for (waiting extends the provider's penalty window), and `gh release create … --notes "$NOTES"`, an inline-body recipe where the release operation mandates `--notes-file` after a scrub whose failure is a hard stop. Both were invisible to every guard. After: 6,581 characters, both contradictions removed, and the inline-body guard widened to see `gh release … --notes` and rescoped to the skill files. Three `sleep 60` sites in all — the third in `references/github-api.md` — are gone.
120
+
121
+ - **Every shipped recipe that posts a body posts the scrubber's output** — before: sixteen recipes across `references/github-api.md`, `references/patterns.md`, the generated tracker references and the review-methodology skill built a body inline — `--body "$(cat <<'EOF' …)"`, `-f body="$BODY"`, `--notes "$changelog"`, `--notes-file CHANGELOG.md` — so the text reached GitHub without passing `redact-secrets.cjs` at all, in the same files that tell an agent the scrub is unconditional. After: each one composes to `$DEVFLOW_BODY_RAW` (release notes to `$DEVFLOW_NOTES_RAW`, or `CHANGELOG.md` read as raw input), runs the scrubber, and posts the scrubbed file through `--body-file` / `-F body=@` / `--notes-file`, chained with `&&` so a non-zero scrubber exit means the post does not happen. The tech-debt archive closes its predecessor with **no comment body**: before, it closed with a `--comment` placeholder reading `(see linked issue)` and then posted the real number in a second comment; after, it creates the successor first and posts one scrubbed archive comment carrying that number, so the close is a close. Reviews write reports and only the Git agent publishes — the review-methodology skill's own comment-creation recipe is replaced by a pointer to `post-review-summary`, where the repo-visibility gate (D10) and the comment-sink scrub (D11) already live, so there is one publication path instead of two. The inline-body guard that polices this folds shell line-continuations before matching (a `--body` four lines below its `gh` verb is one command, not four lines), names its five posting shapes separately so each is proven live by its own known-bad probe, and scans **every installed agent, command, rule and skill** rather than the Git agent's own neighbourhood; the pre-split baseline tree is kept as a permanent known-bad corpus so the widening is proven against text that really did post unscrubbed bodies. Two `gh … --json number` flags that neither `gh issue create` nor `gh pr create` accepts are replaced by deriving the number from the URL each command prints. Every recipe renders the same text it always did; what changes is what reaches the tracker when a body carries a secret — before, a shipped recipe posted it, and after, the post does not happen (#340, #341).
122
+
123
+ - **The installer converges the generated references rather than merging into them** — before: nothing installed generated skill references, because none existed. After: `devflow init` overlays them onto the installed `devflow:git` skill directory with a **converge-not-merge** contract — a shadow-supplied file under `references/tracker/**` that the build manifest does not name is removed, and a shadowed `devflow:git` still receives the canonical GitHub references. The swap is **atomic per unit**: each provider directory (and the flat cross-cutting set) is built under a `.tmp` sibling and promoted by rename, so a per-file failure aborts that unit and leaves the previously installed files byte-unchanged instead of promoting a partial tree. Two new install-time failure modes come with it, both reported rather than silent: a unit that could not be refreshed is named in the install summary (`Could not refresh the generated references for "{provider}" …`), and a **declared reference missing from the build** fails loudly with a `npm run build:mds` hint rather than installing an agent instructed to read a file that is not there.
124
+
125
+ - **The command layer speaks one issue-reference vocabulary** — before: five command hosts each carried their own inline `#N` parsing rule, and the design-artifact naming convention used a `{issue}` placeholder. After: one partial, `_partials/_tracker.mds`, states the grammar and the capture contract once and is imported by `plan`, `implement`, `debug`, `dynamic-build` and `dynamic-plan`; the placeholder vocabulary is `{ISSUE_REF}` (the rendered reference) and `{ISSUE_ID}` (the filesystem-safe form), each site also stating its GitHub rendering so the rendered bytes are pinned. `ISSUE_NUMBER` is kept at all fourteen Code-agent spawn sites. Commands no longer restate a dedup marker literal — the operation owns its marker.
126
+
127
+ - **Byte budgets for the Git spawn collapse to one ceiling, and the loaded-set rows drop with the PR-host split (#326)** — before: the git.md gate was two phase-named constants — a whole-file ceiling and a smaller non-preamble base, kept apart so a Phase-3 preamble revision could be read as a delta from an unraised number — a pairing the Phase-3 refactor introduced and never collapsed, because the file's measured size never dropped far enough below the base to make one constant sufficient. Alongside it, `chars(skills/git/SKILL.md) ≤ 6,600`, and the worst-case tracker spawn's loaded set priced **per provider**, because a provider that loads the tool-call contract must not bill users who never receive it: `≤ 80,200` characters on the GitHub path, `≤ 89,500` under Jira, `≤ 91,700` under Linear. Each of the three was re-baselined once, under explicit authorisation, against the shipped per-operation split. After: the PR-host split cleared enough headroom to collapse the pair into one `chars(dist/agents/git.md) ≤ BUDGET_GIT_MD` ceiling, asserted directly against the whole compiled file — the now-pointless non-preamble companion assertion, and the preamble-chars constant it subtracted, are retired along with the pair they existed to distinguish. The agent is 44,163 characters (824 lines) today, under a ceiling of 44,243. The three per-provider loaded-set ceilings are each re-baselined **down**: `≤ 64,994` on the GitHub path (was 80,200), `≤ 75,344` under Jira (was 89,500), `≤ 75,940` under Linear (was 91,700). A fourth row, `BUDGET_LOADED_SET_PR_HOST` (`≤ 58,306`), prices a spawn that runs one of the nine operations whose mechanics live under `references/pr/`, gated the same way the per-provider rows are; the same maximum with `references/github-api.md` additionally charged is recorded, not gated, because `fetch-review-threads` and `resolve-review-threads` loaded that file long before this split, which moved only the line that names it: charging it would make the row measure a file this work never wrote, and would bury the `pr/` bodies the row exists to measure. A written-exclusion test proves the unexcluded figure would not fit under the gated ceiling. What each row sums is the one-spawn hop closure described below (#376), and every row that carries git.md sits at its measured size plus 80 characters. Every ceiling may be lowered and never raised; each value above replaced a higher one.
128
+
129
+ - **`tests/fixtures/golden/github-status-lines.txt` is frozen from its fifth capture** — the fixture samples prompt-internal process steps, which is precisely the text this refactor relocates, so a re-capture is what a relocation of that text costs. The first followed the D4 invariant/detector cut, which split two of its sampled sentences, so preserving the fixture and making the split were mutually exclusive. The second followed the review-wave condensing of the `**Mechanics:**` pointer lines it samples, and moved only the two byte-count lines that shift when the agent is regenerated. The third followed the per-operation retarget, which moved nine of the twenty-four git-side samples out of `dist/agents/git.md` into generated references. The fourth followed the release-evidence gather rewording, which changed the two lines that extract and gate candidate references (#368). The fifth followed the `check-ci-status` classifier fix, which changed the three lines that fetch and classify checks (#372, #352). Each is a single fixture-only commit under its own explicit authorisation, and each authorisation is spent on the capture it covers — a sixth needs its own. The four user-visible byte-identity claims have their own assertions and are untouched.
130
+
131
+ - **The Git agent is now compiled from an MDS generator host** — before: `src/assets/agents/git.md` was a hand-authored file the installer copied verbatim; the build owned command files only. After: `src/assets/agents/git.mds` declares `output-dir: dist/agents` in a leading steering block and compiles to `dist/agents/git.md`, which was byte-identical to the hand-authored file it replaced at the conversion (66,180 bytes, unchanged SHA-256); the contract/mechanics split is what changes its size. Both agent readers take their directory order from one owner, `agentSourceDirs()` in `src/core/assets.ts` — `dist/agents/`, then `src/assets/agents/`. The installer resolves each declared agent against that list and copies the first hit, throwing with both candidate paths and `npm run build:mds` named when neither directory has it; `loadShippedDefaults()` walks the same list first-wins and warns through its `onWarning` channel when a registry-declared agent has no shipped default in either. The compiled artifact wins for a generated agent and the other 15 agents install exactly as before. The 13 compiled command outputs in `dist/commands/` are byte-unchanged, and the hand-authored `release.md` beside them is untouched — 14 deployed command files in all. Zero user-visible change.
132
+
133
+ - **`npm run build:cli` alone no longer produces installable agents** — before: `build:cli` (TypeScript) plus the shipped `src/assets/agents/*.md` were enough to install every agent. After: an agent authored as a generator host exists only as a `.mds` source until `npm run build:mds` compiles it, so a publish or install path that runs `build:cli` alone would ship without a Git agent. `npm run build` runs both and is unchanged; the packaging and pack-install guards now fail loudly if the compiled agent is missing from the tarball.
134
+
135
+ - **`tests/integration/subagent-skill-preload.test.ts` is excluded from `npm run test:integration`** — before: `vitest.integration.config.ts` declared only an `include` glob, so the file was collected by every integration run, including CI, and no-op'd only where the `claude` binary was absent, through its own `describe.skipIf(!isClaudeAvailable())` guard; on a machine with `claude` installed it spawned live sessions. After: the config carries a real `exclude` entry. The test drives live `claude` sessions against the developer's own `~/.claude` with `--dangerously-skip-permissions` and has previously committed to this repo mid-run, so it is opt-in: set `DEVFLOW_INTEGRATION_ALL=1` to include it. A command-line path alone cannot re-add it — `exclude` is applied at glob time.
136
+
137
+ - **`pin-sonnet-4-6` and `disable-bundled-skills` now default OFF** — before: both flags were in the recommended set with `defaultValue: true`, so a fresh `devflow init` pinned `ANTHROPIC_DEFAULT_SONNET_MODEL` to `claude-sonnet-4-6` and wrote `disableBundledSkills: true` to settings.json, removing Claude Code's built-in skills and commands. After: both are optional flags defaulting to `false`; a fresh install leaves the Sonnet alias and Claude Code's bundled skills untouched. Existing installs keep whatever value their manifest already records (ADR-014 — re-init preserves existing flag values); opt in or out with `devflow flags --enable/--disable pin-sonnet-4-6` and `devflow flags --enable/--disable disable-bundled-skills`.
138
+ - **`devflow tracker --set` converges the whole bundle, in both directions** (#350) — before: `--set` wrote the manifest, the sentinel and the conventions rename; the reference files and the Tracker agent were an install-time concern, so switching providers left the previous provider's mechanics on disk. After: `--set` converges references, stale-conventions rename, manifest, Tracker agent file, attempt counter and sentinel in that fixed order, and it converges in both directions — `--set github` REMOVES what `jira` or `linear` installed. Two branches exit 1 leaving the manifest, sentinel and conventions untouched: `devflow:git` is not installed (the mechanics have nowhere to land), or the reference overlay failed. The overlay is atomic per unit, so a failure names the units that failed instead of claiming nothing moved. `devflow tracker --status` gains a `Mechanics:` line — `installed (N file(s))`, `MISSING — run devflow init`, or `unreadable (<errno>)`, three outcomes rather than two because "no files" and "could not look" have different remedies. `devflow skills list` now says which plugin provides each skill and whether that plugin is selected, and `devflow uninstall --plugin` retains assets on behalf of the plugins the **manifest** records as installed rather than the whole registry — so removing a plugin removes exactly its own skills instead of keeping them alive for a plugin you never installed.
139
+
140
+ - **`git.md`'s eight PR/review operations moved to a provider-independent reference module** (#326) — before: `ensure-pr-ready`, `validate-branch`, `post-review-summary`, `check-ci-status`, `fetch-review-threads`, `resolve-review-threads`, `post-resolution-summary` and `check-merge-readiness` each carried their `**Process:**` body inline in `git.mds`, even though pull requests, PR reviews and PR checks stay on GitHub under every tracker provider — none of those eight bodies was ever a tracker fact. After: each body moves to a new generated module, `src/assets/mds/git/_pr.mds` → `references/pr/{op}.md`, one file set installed **unconditionally under every provider** (unlike `references/tracker/**`, which is `{github} ∪ {selected provider}`-scoped) — named by a fixed literal path that composes nothing from the provider token. Each operation's section in `git.mds` keeps its contract — heading, purpose, `**Input:**`, `**Degradation (D4):**` where the operation has one, the `**Process:**` label, a `**PR mechanics:** load references/pr/{op}.md` pointer, and `**Output:**` — with `ensure-pr-ready` carrying two pointers (PR mechanics plus the provider's own Mechanics pointer): the provider reference owns step 4b, the verified issue and its link line, and publishes that line through the PR-host file's open-PR lookup and scrub-then-edit. `check-merge-readiness` reuses `check-ci-status`'s classification and names `references/pr/check-ci-status.md` for loading. Deliberately retained in `git.md` itself, each for a guard that reads the agent alone: both summary ops' sentence naming `references/publication-gate.md` (D10); `post-resolution-summary`'s op-local non-reproduction clause; `resolve-review-threads`' step 3, the D9 gate application; and `## Comment-sink scrub (D11)`, which never left the agent. The move took `dist/agents/git.md` from 58,100 characters (917 lines) to 45,103 (801 lines); it is 44,163 characters (824 lines) today. The unconditional set is nine files — `update-pr-evidence` joined the eight when test-plan evidence landed — so a `github` install carries 24 generated references and a `jira`/`linear` install 36, and the packed reference manifest across all providers totals 47 files. `references/pr/` converges the same way `references/tracker/` already did — a stale file under `pr/` is removed on re-install or `devflow tracker --set`, a deleted generated document is put back, and a provider switch never adds or removes it (it is wanted under every provider). The installer swaps `pr/` whole, as it swaps a provider directory, staging it and its backup under `tracker/` so a crash that strands either is pruned by the next run. It refuses a converged root — `tracker/` or `pr/` — that is a symlink or otherwise not a real directory, and reports the refusal rather than writing or pruning through it. The frozen fixture `tests/fixtures/golden/github-status-lines.txt` is byte-unchanged: six entries joined `STATUS_LINE_REFERENCE_FILES`, two samples became straddle splits and one a three-part straddle, but every sampled byte still exists in the same order. Zero user-visible change to any operation's contract.
141
+
142
+ - **Byte budgets price every reference a spawn loads** (#367, #376) — before: each tracker loaded-set row summed an operation's own load and then added the largest single mechanics file again as a separate term. That term counted a file already inside the one-spawn load, and it covered the one hop no row priced — `setup-task` step 1c loading the `ensure-traceable-issue` mechanics when an issue is required — only by coincidence. After: each row prices one spawn's closure, per provider: the operation's own load plus the load of every operation its reference bodies hop to (`setup-task` → `ensure-traceable-issue`; `gather-release-evidence` and `associate-release` → `backlink-shipped-issues`; on Linear also `post-wave-report` → `backlink-shipped-issues`). A default-deny closure scan reads every tracker, PR-host and cross-cutting reference body: an operation named there is either a priced hop or one of 28 informational mentions, each pinned to a verbatim anchor with the reason it is not a load, and a mention that is neither fails the byte-budget suite, as seeded probes P1–P9 prove. The largest-file figure is still printed, as a recorded row and never a term. The worst spawn on every provider is now `setup-task` with its conventions and issue hops, and every row that carries `git.md` fell to its measured size plus 80 characters, with none raised. Earlier in the series, text an operation needs only while it runs — verdict and input definitions, section lists, the untrusted-input rule — moved out of the always-loaded agent into the references those operations already load, while every Input line and Output template stayed in the agent (#367).
143
+
144
+ - **`/dynamic-build` builds each ticket on the branch `setup-task` created** (#376) — before: the engine told every phase after setup to work on a `ticket/{slug}` name it minted itself, which was not the branch `setup-task` had created. After: `setup-task` names and creates the branch, the engine takes it from setup-task's `- **Branch name**:` output and uses it verbatim through the wave merge, and a ticket for which setup-task reports no branch stops as ESCALATED (`branch-missing`) rather than being built on a name devflow invented — the Git agent names branches; the engine and the scripts never do. The preflights of `/dynamic-tickets`, `/dynamic-plan` and `/dynamic-build` name no single tracker CLI: filing and fetching go through the Git agent, which resolves the provider and reports `TRACEABILITY: DEGRADED ({reason})` when it cannot.
145
+
146
+ - **Release tracing and release-evidence gathering read commit messages the same way** (#376) — before: `release-trace.cjs` scanned a commit's subject as git renders it, which folds a wrapped subject onto one line, while the `gather-release-evidence` mechanics read each message line by line — so a closing keyword ending a subject's first line and a reference opening its second traced the commit in one and was never collected by the other. After: both read the full message (`git log --format=%B`) line by line and agree, as a parity test pins: a reference anywhere in the message, a wrapped subject's second line included, is found by both, and a keyword and a reference split across that wrap are found by neither — so keep a closing keyword and its reference on one line.
147
+
148
+ - **Release evidence and issue setup accept the shapes real repositories use** (#368) — GitHub release evidence finds the issues closed by squash-merged pull requests through one bounded merged-PR listing mapped onto the tag range, reporting `DEGRADED` when the listing does not cover the range and `INDETERMINATE` when it hits its 200 cap. Closing keywords are read in every verb form (close, fix, resolve) with trailing punctuation, parentheses, colons and comma lists, so `Closes #12.` and `(Resolves #12)` count. `setup-task` accepts `#42` as well as `42`, and a malformed reference degrades rather than creating a duplicate issue; Jira and Linear `setup-task` learn and apply branch conventions as GitHub's does; and acceptance-criteria extraction handles numbered items, lower-case headings, CRLF line endings and a section at the end of the issue.
149
+
150
+ - **The `Branch token` handoff value is the branch name** (#376) — before: `setup-task` and `fetch-issue` both emitted `- **Branch token**: {token}` under `### Handoff Values`, a placeholder nothing defined. After: `setup-task` emits the branch it created and `fetch-issue` the branch its `### Suggested Branch` section names, so the value a caller forwards is always a real branch name.
151
+
152
+ - **The documentation covers the evidence policy end to end** (#376) — the README gains Evidence policy and Test-plan evidence sections and shows the real `.gitignore` carve-out; `docs/commands.md` brings every command up to the evidence-policy workflows; the release-process reference documents release traceability and states what the policy does not do — a CI `workflow_dispatch` release bypasses `/release` and its trace gate, nothing is enforced server-side, and nothing merges automatically — along with the races that remain; and the compliance skill now says it shapes what a compliance review checks, while tickets, test plans, approvals and release traces follow the evidence policy, so a missing one is never a compliance finding.
153
+
154
+ - **Memory, learning and knowledge are machine-wide switches** (#379) — before: each feature had two switches that disagreed. `devflow init` and the feature commands recorded the choice in `~/.devflow/manifest.json`, but the hooks and the knowledge write-back step read only the per-repo `memory`, `learning` and `knowledge` keys in `.devflow/config.json`, and treated a missing file as on. `devflow memory|learning|knowledge --enable/--disable` wrote the config of the repo you ran them in (and did nothing outside a git repo), so each repo had to be opted out one at a time. `devflow memory --disable` left the memory hooks registered. After: `features.memory`, `features.learning` and `features.knowledge` in `~/.devflow/manifest.json` are the only switch, and every hook and command reads it, in every repo and in non-git directories. A missing manifest or a missing key still means on. The per-repo keys are no longer read, and init's next write of `.devflow/config.json` removes them, which leaves `reviewPublication` and the `tracker` override. The three commands now switch the feature for the whole machine from any directory, report the machine-wide value with `--status`, and exit 1 with "run `devflow init` first" when devflow is not installed. `--disable` also clears the current repo's pending queue. `devflow memory --disable` now removes the memory hooks from `settings.json` and `--enable` adds them back, the same way `devflow init --no-memory` / `--memory` do. init takes its memory, learning and knowledge defaults from the manifest instead of the current repo, so a stale per-repo value cannot turn a feature back on or off. `init --hud-only` over an existing install keeps every other manifest value and only turns the HUD on. The older `features.decisions` and `features.kb` manifest keys still count as the learning and knowledge switches, but a current key wins over them.
155
+
156
+ - **Routing runtime pinned to `subswitch@0.5.0`** (#384) — before: `subswitch@0.4.0`. After: `0.5.0`, which changes some model aliases. `sol` and `luna` now resolve to `gpt-6-sol` and `gpt-6-luna` (`terra` stays on `gpt-5.6-terra`), so an agent set to `sol` or `luna` moves to the GPT-6 model at its next session. To stay on 5.6, pin the exact id with `devflow agents --set <agent> --model gpt-5.6-sol` (or `gpt-5.6-luna`). `astra` (`gpt-6-astra`) is new and shows up in the agents picker on its own. `gpt-5.5` is retired ahead of its Codex shutdown on 2026-10-14: the agents picker no longer offers it, and an agent already pinned to it keeps routing by exact id until that date. The proxy still writes the same `proxy-routing.json` shape. The runtime's new `codexIngress` key is left out on purpose because devflow's relay serves Claude Code only. The runtime requires Node `^22.15.0 || >=24`, while devflow still declares `>=22.0.0`. On Node 22.0–22.14, npm prints `EBADENGINE` warnings and installs anyway, but an `engine-strict=true` install fails. The relay, `doctor` and model discovery were run on Node 22.0.0 and worked.
157
+
158
+ ### Fixed
159
+
160
+ - **The comment-posting non-reproduction rule was unreachable from three of the four operations it governs** (#328) — before: the rule — no comment-posting operation reproduces verbatim content from an `<external-thread>` body or an `<untrusted-issue-body>` — was stated once, inline, inside `post-resolution-summary`'s compose step; a reader of `post-review-summary`, `post-wave-report` or `backlink-shipped-issues` never reached that sentence. After: the cross-op rule is a named sub-bullet of Principle 8 — "Never reproduced in a posted body" — naming all four operations, where Principle 8's containment contract already lives for the whole agent; `post-resolution-summary` keeps its own op-local clause stating only what that operation's own body must not reproduce, readable from the op's own section as the guard that reads it there requires.
161
+
162
+ - **Shipped recipes posted bodies the scrubber had never seen** (#340, #341) — before: sixteen recipes across `references/github-api.md`, `references/patterns.md`, the generated tracker references and the review-methodology skill composed a body inline and handed it straight to `gh`, so an agent following the shipped text reached the tracker without `redact-secrets.cjs` running at all — in the same files that tell it the scrub is unconditional. The tech-debt archive was the same defect in a second shape: it closed its predecessor with a `--comment` placeholder and posted the real number in a separately-composed follow-up. After: each one composes to `$DEVFLOW_BODY_RAW` (release notes to `$DEVFLOW_NOTES_RAW`), runs the scrubber, and posts the scrubbed file through `--body-file` / `-F body=@` / `--notes-file`, `&&`-chained so a non-zero scrubber exit means the post does not happen. This sink has no erasure path — a comment lands at the repository's visibility, GitHub keeps edit history, and notifications have already fired — so anything that got through had to be answered by rotating the credential, not by editing the comment. The **Changed** entry above carries the full inventory and the guard that now polices it.
163
+
164
+ - **`/debug #42` wrong Git-op spawn key** — before: `debug.mds` passed `ISSUE: {issue number}` to the `fetch-issue` Git operation, which declares `ISSUE_INPUT:`; the key mismatch meant no issue was ever fetched. After: `debug.mds` passes `ISSUE_INPUT: {issue reference}` — the key the op declares. (AC-0.1)
165
+
166
+ - **`/plan` with issue references: issue body never fetched** — before: `/plan #42` parsed the issue reference but never retrieved it; the design was built without the issue content. After: `/plan #42` spawns the Git agent with `OPERATION: fetch-issue`; `/plan #12 #15 #18` uses `OPERATION: fetch-issues-batch` (≤50 issues, `TRUNCATED ({n} not processed)` beyond the cap). (AC-0.3)
167
+
168
+ - **`fetch-issue`/`fetch-issues-batch`: all remote-sourced fields now contained** — before: issue title, body, labels, acceptance criteria, and dependencies reached Design agents unwrapped, with no `<untrusted-issue-body>` containment tag of any kind. After: all remote-sourced fields per issue are wrapped in a single `<untrusted-issue-body>` block with a data-only note appended after the closing marker; the `### Suggested Branch` slug (derived locally from the title, not attacker-controlled) remains outside the block. (AC-0.10)
169
+
170
+ - **`resolution-summary.md` `Tracked = (pending)` fields now state the reason** — before: four sites in `resolve.mds` wrote a bare `(pending)` with no explanation of what it was pending on, making the field ambiguous in every resolution summary. After: all four sites qualify the pending state with its reason — backfill after Phase 9 manage-debt, or `TRACEABILITY: DEGRADED ({reason})` on failure — making the field self-explaining and consistent with the degradation path that already named the reason. (AC-0.6)
171
+
172
+ - **`release.md` promised a `close milestone` step that does not exist** — before: `release.md` listed a post-release "close milestone" step; no such Git operation existed, so the step was silently a no-op and the command description was false. After: the `close milestone` reference is removed. (AC-0.14)
173
+
174
+ - **`/resolve` D9 thread-resolution gate narrowed** — before: `resolve.mds` authorised the Git agent to auto-resolve review threads on any of three verdicts — `FIXED`, `FALSE_POSITIVE`, or `BY_DESIGN`. After: auto-resolution is authorised only when the verdict is `FIXED` and `commit_sha` is non-empty — matching the narrower D9 contract the Git agent had always enforced, closing a live divergence. (PF-024)
175
+
176
+ - **Issue-body containment: three gaps closed** — before: (a) `setup-task` (`git.md`) — the operation `/implement` actually uses and the highest-traffic issue path in the product — emitted issue title, description, and acceptance criteria as bare bullets, while Principle 8 claimed all remote-originated bodies were wrapped; (b) the `fetch-issues-batch` output template demonstrated wrapping on the first issue only, with the second issue shown as a bare `...` elision and no instruction that the wrapper repeats — leaving up to 49 of the 50-issue cap plausibly uncontained; (c) no operation addressed the case where remote content itself contains the literal `</untrusted-issue-body>` closing marker, allowing an issue author to close the block early and inject text into devflow-authored context. After: `setup-task` wraps all remote-sourced fields in `<untrusted-issue-body>` (locally-derived fields — issue number and branch name — stay outside, matching `fetch-issue`'s model); the `fetch-issues-batch` output template now shows the full wrapper on both the first and second issue, with an explicit per-issue statement that the wrapper repeats for every entry; Principle 8 mandates neutralising any closing marker found in remote content before wrapping, with pointers from all four affected operations. (AC-0.10)
177
+
178
+ - **`/plan` issue-fetch contradicted its own spawn ban** — before: `plan.mds` declared "Do not spawn any agents until Gate 0 is confirmed" with no exception, directly contradicting the Step 0 issue fetch that must precede Gate 0; a session honouring the ban could silently skip the fetch, making the AC-0.3 fix a no-op. After: the line names the Step 0 issue fetch as its sole exception. (AC-0.3)
179
+
180
+ - **`learn-conventions` left `.devflow/conventions.md` untracked** — before: the `learn-conventions` Git operation wrote `.devflow/conventions.md` — a git-tracked carve-out path — but included no commit step, leaving `?? .devflow/conventions.md` in `git status` on every fresh project. After: when conventions apply — under a `required` evidence policy — `setup-task` step 4b commits `.devflow/conventions.md` after `git checkout -b` via a scoped pathspec (`commit --only -- .devflow/conventions.md`; never `git add -A`, never push, never force, never amend), on the feature branch and never on the base branch, reporting `CONVENTIONS_COMMIT: …` non-blockingly; `learn-conventions` no longer commits.
181
+
182
+ - **`devflow init` left `.claudeignore` untracked** — before: `devflow init` wrote `.claudeignore` into any git repo it ran in but never ignored the file, leaving `?? .claudeignore` in `git status` on every fresh install. After: block presence is detected only by the devflow-unique `!.devflow/conventions.md` line (the marker file is `.devflow/.root-gitignore-configured-v5`); a user-authored `.claudeignore` or `!.claudeignore` line is respected — the block is appended without its own `.claudeignore` line and a user's un-ignore is never overridden; the TypeScript function and the `ensure-root-gitignore` shell hook produce byte-identical, idempotent output.
183
+
184
+ - **`/plan`, `/debug`, and the dynamic-build wave reader did not handle `TRACEABILITY: DEGRADED`** — before: all three callers treated a `TRACEABILITY: DEGRADED` return from the Git agent the same as a successful fetch. After: `/plan` warns, carries the line verbatim into the report's traceability section, and runs Gate 0 with the bare issue reference as sole context; `/debug` reports the line verbatim and asks for the bug description before generating hypotheses; the wave reader returns empty ready/blocked sets with the DEGRADED rationale and the wave stops. (AC-0.6)
185
+
186
+ - **`fetch-issue` and `fetch-issues-batch` mishandled `#`-prefixed issue references** — before: `fetch-issue` step 1 read `If numeric, fetch directly; if text, search and select first open match` with no `#` handling, so `#42` took the text-search path and resolved to the first open match for the literal string `#42` — an unrelated issue, or none — never a direct fetch of issue 42; `fetch-issues-batch` step 1 said only `Parse ISSUE_REFS into a list of issue numbers`, leaving a `#`-prefixed token unspecified. After: both strip a leading `#` before parsing (`#42` ≡ `42`). (AC-0.3)
187
+
188
+ - **`fetch-issues-batch` aborted on one unresolvable reference** — before: a null GraphQL alias (an unresolvable reference) in a batch could abort the entire fetch. After: the null alias is dropped and reported as `NOT_FOUND ({refs})` alongside any `TRUNCATED` note; comments are not fetched in batch mode. (AC-0.3)
189
+
190
+ - **Jira issue keys were matched case-sensitively** (#350) — before: a reference typed `proj-12` was not the same string as `PROJ-12`, so an issue the user had named went unrecognised and the run degraded. After: the project key is normalised to ASCII upper case at the one place the grammar is stated, and the same alphabet (`^[A-Z][A-Z0-9_]{1,9}$`) is used by the Git agent, the Tracker agent and every provider mechanics module. This is a behaviour change, not only a fix: a lower-case Jira key that previously failed to match now resolves.
191
+
192
+ - **`check-ci-status` reported `NO_CI` on every pull request, and merge readiness could fall through to READY** (#352, #372) — before: the operation asked `gh pr checks` for a `conclusion` field `gh` does not have, so the command failed and every PR read `NO_CI` — the CI gates in `/implement` and `/resolve` never actually checked CI — and its classifier had no arm for a check that was requested but not yet started, a gap `check-merge-readiness` resolved to READY because READY was its default. After: it reads `bucket`, treats exit 8 (checks pending) as a result, and classifies every case: pending, then failing, then passing (every check `pass` or `skipping`, at least one `pass`), and otherwise a new `INDETERMINATE`, which both CI gates poll like `PENDING` before reporting "CI status unknown — verify manually before merging". READY is one positive conjunction of every check, and anything unclassified is `NOT_READY (status unknown)`.
193
+
194
+ - **The orchestrator charter overrode each agent's configured model** (#370) — before: the session-start charter and the per-prompt reminder sorted work into haiku, sonnet and opus tiers, which steered the orchestrator into overriding the model and effort each agent is configured with, and the charter carried a maintainer-only HTML comment that was injected into every session. After: both route work to the roster agent that fits it — Explore and Skim to search and orient; Code, Validate and Git to execute against a spec; Design, Research, Review and Triage to analyse — and name no model, so each agent runs on its own configured model, a `devflow agents` override included. The HTML comment is gone.
195
+
196
+ - **Only the first `/resolve` on a pull request ever posted a resolution summary** (#369) — before: the dedupe matched a bare marker prefix, so the first cycle's comment satisfied it for every later cycle and cycles two onward reported "already posted" while the PR showed review findings with no matching resolution. After: the dedupe keys on the run's own timestamp, so every cycle posts, and the `/resolve` report gains an `### Evidence Posts` section — resolution comment, publication mode, thread replies, push. In the same pass: a failed visibility probe reports `STUB (visibility undeterminable)` instead of calling the repository public, and still fails closed; the paths a posted summary names are repository-relative, so no absolute local path leaks into a PR; a devflow marker planted by an untrusted author no longer hides a review thread; and a fork PR the run cannot push to degrades with `cannot push to fork`, skips the push and CI steps, and still posts its comments.
197
+
198
+ - **`/plan` followed by `/implement` could file the same issue twice, and SEQUENTIAL `/implement` skipped the CI gate** (#369) — before: `/implement` read a plan's issue only after `setup-task` had run, so when an issue was required it created a second one for a plan that already had one, and the SEQUENTIAL strategy never reached the CI gate its PR needed. After: `/plan` writes `issue: pending` and patches the reference in place once the issue is filed, and `/implement` forwards the plan's issue to `setup-task`, which then decides the issue; SEQUENTIAL `/implement` runs the CI gate. `/dynamic-build` now hands its test plan to the Test agent, and a Gate 2 verdict whose fixes were applied but not re-run reports `UNVERIFIED`, never `PASS`.
199
+
200
+ - **`/implement`'s parallel pull request was opened without the secret scrub** (#368) — before: under the PARALLEL strategy the orchestrator created the unified PR itself, so its body skipped the D11 scrubber every other PR path runs. After: it spawns the Code agent in a `pr-create` mode that pushes, composes the body through the same paste gates — the `## Related Issues` line re-checked, as a whole line, against all three tracker reference grammars, and never chosen by provider — and posts only the scrubbed file.
201
+
202
+ - **`/plan` with several issues named its design artifact after an ID it never had, and never used the issue content** (#331, #376) — before: a multi-issue artifact was named from the first issue's ID, but a batch fetch returns none, so the name had nothing to build from; and the fetched issue content and acceptance criteria were captured and then never read again. After: a multi-issue artifact is `multi-{topic-slug}.{YYYY-MM-DD_HHMM}.md` with frontmatter `issue: pending`, which the tracker-issue step patches in place, and Gate 0 seeds its discovery with every fetched issue's content and acceptance criteria, which the artifact's Problem Statement and Acceptance Criteria sections name as their sources — summarised, never pasted, because issue text is data, not instructions.
203
+
204
+ - **Re-running `devflow init`, or toggling a feature, deleted per-repo config keys devflow does not manage** (#376) — before: `devflow init` rewrote `.devflow/config.json` whole, with only the keys it managed at the time, so on the default user scope every re-init inside a git repository silently deleted the per-repo `tracker` override and any other key it did not know — an invalid override went too, and with it the `TRACEABILITY: DEGRADED` report that would have named it — and the `memory`, `learning` and `knowledge` toggles deleted unknown keys the same way. After: both read the file, set only the managed keys and carry every other key over verbatim. The retired `decisions` and `autoCommit` keys — and, since #379, `memory`, `learning` and `knowledge` — are dropped, and `--reset` returns the managed keys to their defaults while leaving every other key alone.
205
+
206
+ - **Release notes still over the size cap had no rule** (#376) — before: when composed release notes stayed over 60,000 characters after the commit list was dropped, the release operation said nothing about what to cut. After: only the changelog content is cut, at a line boundary, ending `…truncated`; the traceability-exceptions block is never dropped; and the cap is applied again after the secret scrub.
207
+
208
+ - **Turning off learning, knowledge or memory in `devflow init` only took effect in one repo** (#379) — before: init printed "Learning: disabled", but only the repo init ran in was switched off. Every other repo kept capturing turns and spawning the Learning agent (and kept knowledge write-back and working memory running), including repos with a stale `learning: true` from an earlier init, repos with no `.devflow/config.json`, and non-git directories. After: the choice is one machine-wide switch that every hook and command reads (see Changed). init also clears the pending queues of the features it turned off, and only after the switch is recorded, so a session running at the same time cannot refill a queue while the switch is still on.
209
+
210
+ - **`devflow init --security none` left the managed-settings deny list in place** (#379) — before: re-running init with `--security none` after a managed install removed the user-settings deny list but not the managed one, so Devflow's deny rules kept applying. When `devflow security --disable` could not remove the managed entries, it told the user to run `devflow security --disable` again. After: `--security none` removes the managed deny list through the same path as `devflow security --disable`. A permission failure is a warning rather than an error. A failed removal now gives two workable fixes: re-run in an interactive terminal and accept the sudo prompt, or have an administrator remove the entries from the named file. The sudo calls run with an argument list and no shell.
211
+
212
+ - **A personal routing-runtime config leaked into model discovery** (#384) — before: `subswitch@0.5.0` merges `~/.config/subswitch/config.json` into every config load that does not name a config file, and model discovery's `models --json` call is one of those. A personal alias therefore appeared in the `devflow agents` picker even though devflow's relay, which always runs with its own explicit config, would never route it, and a malformed personal file made live discovery fail. After: discovery points the runtime's user-config lookup at devflow's cache directory, so the picker lists only the runtime's registry models and a personal config cannot break it.
213
+
214
+ - **Knowledge-base and conventions commits always failed** (#381) — before: the Knowledge agent and the Git agent's `learn-conventions` step were told to run `git commit --only -- <paths> -m "<msg>"`. Git reads everything after `--` as a path, so it took `-m` and the message as paths and refused the commit (`error: pathspec '-m' did not match any file(s) known to git`). Both steps are non-blocking, so the failure was quiet: every `.devflow/features/` write-back and every `.devflow/conventions.md` was left uncommitted, reported only as `KB_COMMIT: failed` / `CONVENTIONS_COMMIT: failed`. After: both instructions put `-m "<msg>"` before `--`, and a guard test fails the build if any shipped command, agent or skill puts `-m`/`--message` after `--`.
215
+
216
+ **Upgrade (`.gitignore`)**: no action required. The devflow-managed carve-out block advances from marker `v3` to `v5` and gains two lines: `!.devflow/policy.json`, so a team can commit its evidence policy, and `.claudeignore`. The next `devflow init` or session-start hook detects the existing block, appends the missing lines in place — an existing block keeps its old comment — and removes the older marker files once `v5` is stamped. A `v2`-era block also receives the `!.devflow/conventions.md` re-include in the same pass, including one a user has extended with their own `.claudeignore` line. A user who had already committed their own `.claudeignore` is unaffected — gitignore has no effect on tracked files. A repo whose `.gitignore` already carries a `.claudeignore` or `!.claudeignore` line of its own receives every carve-out line except `.claudeignore` — that one line is left to the project, so an `!.claudeignore` un-ignore is never reversed; all other carve-out lines always land. Only `.gitignore` lines are read: whether `.claudeignore` is already tracked is not detected and does not change what is written.
217
+
218
+ **Upgrade (evidence policy)**: nothing is required — with no committed file each machine resolves the policy its gates effectively ran under before: `required` where compliance is enabled, `standard` elsewhere. To make the policy a team decision, commit `.devflow/policy.json` on the default branch (`devflow compliance --enable` or `--set` prints one) and guard it like any other policy file, for example with a CODEOWNERS line `/.devflow/policy.json @your-org/maintainers`; a committed `standard` still does not lower a machine with compliance enabled. What changes under `required`: `/implement` missing a ticket link or a test plan asks, before writing code, whether to record a self-attested exception or stop; `/dynamic-tickets` files the tracking issue and one issue per ticket — it files none under `standard` — and `/dynamic-build` stops a ticket that has none; `reviewPublication: off` becomes the counts-only `stub` for review summaries and evidence comments; merge readiness stays `NOT_READY` until a trusted non-author approves, which a single-maintainer repository never reaches; and a release range of more than 500 first-parent commits hits the trace bound, so its coverage is unknown and asked about. Under either policy, `/dynamic-plan` test plans are TP lines: `/dynamic-build` does not read a plan in the older JSON test-plan format and notes `Test plan: missing or malformed` for it — re-run `/dynamic-plan` to get TP lines. The default `reviewPublication: auto` posts a counts-only evidence comment even on a private repository; `full` gives the full table, and posts review and resolution summaries in full without the visibility probe, on a public repository too. A repository whose tags are not `vX.Y.Z` or `X.Y.Z` has no last release for `/release` to trace from, so its version analysis starts at the first commit.
219
+
220
+ **Upgrade (feature switches)**: no action is needed unless you turned memory, learning or knowledge off in only some repos. The per-repo values are now ignored, and each feature follows its machine-wide value in `~/.devflow/manifest.json`, which is on unless you turned it off at init or with `devflow <feature> --disable`. Run `devflow memory --status`, `devflow learning --status` and `devflow knowledge --status` to see what is in effect, then set each one with `--enable` / `--disable`. The stale keys in `.devflow/config.json` do no harm and are removed on the next `devflow init`.
221
+
222
+ ### Removed
223
+
224
+ - **The transition-era test scaffolding** (#350) — the containment exemption registry, the byte-copied baseline reference tree and the guard census are gone. Each addressed a line range of a frozen fixture that recorded what the tracker refactor was moving away from; with the end state in place they address nothing. The five live controls that lived alongside them — generated-reference structural parity, batch-first release evidence, GitHub-path reachability and the two single-authority registries — were re-homed into `tests/tracker/reference-reachability.test.ts` and `tests/tracker/single-authority.test.ts` **before** the deletion, and every known-bad probe that read the baseline tree was re-pointed at a named sample rather than dropped.
225
+
226
+ **Covered structurally, in CI only**: the Jira and Linear paths, `full` publication on a private repository and pull requests from forks are verified by structural and CI tests — grammar, closure and parity guards over the generated mechanics, and scripted `gh` and `git` fakes — not by live runs against a Jira or Linear workspace, a private repository or a fork.
227
+
228
+ ### Breaking Changes
229
+ - **Per-repo `memory`, `learning` and `knowledge` keys in `.devflow/config.json` are ignored** (#379): the machine-wide `features.*` values in `~/.devflow/manifest.json` decide instead, and `devflow init`'s next write of the config removes those keys. A repo that relied on `learning: false` (or `memory`/`knowledge: false`) to opt out now follows the machine-wide switch.
230
+ - **`devflow memory|learning|knowledge --enable/--disable` switch the feature for the whole machine** (#379): a single repo can no longer opt out on its own, and the commands never write a repo config. Scripts that ran them inside one repo to scope the change now affect every repo.
231
+ - **`devflow init --hud-only` no longer resets an existing manifest** (#379): over an existing install it keeps every recorded feature and plugin value and only turns the HUD on. Before, it recorded every other feature as off and `plugins: []`.
232
+
233
+ ---
234
+
8
235
  ## [2.4.0] - 2026-09-01
9
236
 
10
237
  ### Added
@@ -1235,6 +1462,8 @@ devflow init
1235
1462
  ---
1236
1463
 
1237
1464
  [Unreleased]: https://github.com/dean0x/devflow/compare/v2.0.0...HEAD
1465
+ [3.0.0]: https://github.com/dean0x/devflow/compare/v2.5.0...v3.0.0
1466
+ [2.5.0]: https://github.com/dean0x/devflow/compare/v2.4.0...v2.5.0
1238
1467
  [2.4.0]: https://github.com/dean0x/devflow/compare/v2.3.0...v2.4.0
1239
1468
  [2.3.0]: https://github.com/dean0x/devflow/compare/v2.2.0...v2.3.0
1240
1469
  [2.2.0]: https://github.com/dean0x/devflow/compare/v2.1.0...v2.2.0