devflow-kit 3.4.0 → 3.5.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 (186) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -4
  3. package/dist/agents/code-edit.md +133 -0
  4. package/dist/agents/code-fix.md +165 -0
  5. package/dist/agents/code.md +39 -182
  6. package/dist/agents/design.md +0 -1
  7. package/dist/agents/git.md +31 -693
  8. package/dist/agents/knowledge.md +8 -2
  9. package/dist/agents/research.md +6 -0
  10. package/dist/agents/review.md +0 -4
  11. package/dist/agents/scrutinize.md +3 -0
  12. package/dist/agents/skim.md +4 -0
  13. package/dist/agents/triage.md +8 -1
  14. package/dist/cli/commands/behavior-prompts.js +77 -0
  15. package/dist/cli/commands/behavior.js +123 -0
  16. package/dist/cli/commands/flags.js +8 -2
  17. package/dist/cli/commands/init-seed.js +7 -3
  18. package/dist/cli/commands/init.js +72 -5
  19. package/dist/cli/commands/rules.js +16 -0
  20. package/dist/cli/commands/tracker.js +112 -15
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +5 -5
  23. package/dist/commands/code-review.md +14 -9
  24. package/dist/commands/debug.md +25 -4
  25. package/dist/commands/dynamic-build.md +28 -38
  26. package/dist/commands/dynamic-plan.md +11 -9
  27. package/dist/commands/dynamic-profile.md +3 -3
  28. package/dist/commands/dynamic-tickets.md +12 -10
  29. package/dist/commands/explore.md +3 -3
  30. package/dist/commands/implement.md +91 -125
  31. package/dist/commands/plan.md +15 -8
  32. package/dist/commands/release.md +3 -2
  33. package/dist/commands/research.md +16 -9
  34. package/dist/commands/resolve.md +20 -15
  35. package/dist/commands/self-review.md +2 -2
  36. package/dist/core/agent-frontmatter.js +101 -0
  37. package/dist/core/compliance-compose.js +6 -4
  38. package/dist/core/feature-switch.js +23 -2
  39. package/dist/core/flags.js +111 -0
  40. package/dist/core/manifest.js +11 -3
  41. package/dist/core/mds-variants.js +68 -26
  42. package/dist/core/plugins.js +42 -65
  43. package/dist/core/project-paths.js +0 -4
  44. package/dist/core/proxy-log.js +1 -1
  45. package/dist/core/proxy-state.js +1 -1
  46. package/dist/core/tracker.js +120 -2
  47. package/dist/learning-off/agents/code-edit.md +129 -0
  48. package/dist/learning-off/agents/code-fix.md +161 -0
  49. package/dist/learning-off/agents/code.md +37 -180
  50. package/dist/learning-off/agents/design.md +0 -1
  51. package/dist/learning-off/agents/knowledge.md +8 -2
  52. package/dist/learning-off/agents/research.md +6 -0
  53. package/dist/learning-off/agents/review.md +0 -4
  54. package/dist/learning-off/agents/scrutinize.md +3 -0
  55. package/dist/learning-off/agents/skim.md +4 -0
  56. package/dist/learning-off/agents/triage.md +8 -1
  57. package/dist/learning-off/commands/bug-analysis.md +3 -3
  58. package/dist/learning-off/commands/code-review.md +12 -7
  59. package/dist/learning-off/commands/debug.md +22 -2
  60. package/dist/learning-off/commands/dynamic-build.md +27 -37
  61. package/dist/learning-off/commands/dynamic-plan.md +10 -8
  62. package/dist/learning-off/commands/dynamic-profile.md +2 -2
  63. package/dist/learning-off/commands/dynamic-tickets.md +11 -9
  64. package/dist/learning-off/commands/explore.md +1 -1
  65. package/dist/learning-off/commands/implement.md +89 -123
  66. package/dist/learning-off/commands/plan.md +13 -6
  67. package/dist/learning-off/commands/release.md +3 -2
  68. package/dist/learning-off/commands/research.md +14 -7
  69. package/dist/learning-off/commands/resolve.md +18 -13
  70. package/dist/skills/git/references/learn-conventions.md +27 -3
  71. package/dist/skills/git/references/pr/check-ci-status.md +19 -0
  72. package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
  73. package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
  74. package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
  75. package/dist/skills/git/references/pr/post-review-summary.md +20 -0
  76. package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
  77. package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
  78. package/dist/skills/git/references/pr/validate-branch.md +26 -0
  79. package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
  80. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
  81. package/dist/skills/git/references/tracker/github/create-release.md +30 -1
  82. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
  83. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
  84. package/dist/skills/git/references/tracker/github/fetch-issue.md +36 -1
  85. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +47 -1
  86. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +25 -0
  87. package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
  88. package/dist/skills/git/references/tracker/github/post-wave-report.md +15 -1
  89. package/dist/skills/git/references/tracker/github/setup-task.md +41 -1
  90. package/dist/skills/git/references/tracker/jira/associate-release.md +24 -0
  91. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
  92. package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
  93. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
  94. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
  95. package/dist/skills/git/references/tracker/jira/fetch-issue.md +36 -1
  96. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +47 -1
  97. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +25 -0
  98. package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
  99. package/dist/skills/git/references/tracker/jira/post-wave-report.md +15 -1
  100. package/dist/skills/git/references/tracker/jira/setup-task.md +41 -1
  101. package/dist/skills/git/references/tracker/linear/associate-release.md +24 -0
  102. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
  103. package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
  104. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
  105. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
  106. package/dist/skills/git/references/tracker/linear/fetch-issue.md +36 -1
  107. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +47 -1
  108. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +25 -0
  109. package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
  110. package/dist/skills/git/references/tracker/linear/post-wave-report.md +15 -1
  111. package/dist/skills/git/references/tracker/linear/setup-task.md +41 -1
  112. package/dist/targets/claude-code/agent-type-owner.js +164 -0
  113. package/dist/targets/claude-code/behavior-install.js +111 -0
  114. package/dist/targets/claude-code/installer.js +69 -2
  115. package/dist/targets/claude-code/learning-install.js +9 -1
  116. package/dist/targets/claude-code/tracker-install.js +33 -16
  117. package/dist/targets/claude-code/tracker-servers.js +107 -0
  118. package/package.json +4 -3
  119. package/src/assets/agents/_partials/_code.mds +109 -0
  120. package/src/assets/agents/code-edit.mds +81 -0
  121. package/src/assets/agents/code-fix.mds +113 -0
  122. package/src/assets/agents/code.mds +18 -222
  123. package/src/assets/agents/design.mds +0 -1
  124. package/src/assets/agents/evaluate.md +7 -1
  125. package/src/assets/agents/explore.md +58 -0
  126. package/src/assets/agents/git.mds +31 -693
  127. package/src/assets/agents/knowledge.mds +8 -2
  128. package/src/assets/agents/learning.md +19 -69
  129. package/src/assets/agents/research.mds +6 -0
  130. package/src/assets/agents/review.mds +0 -4
  131. package/src/assets/agents/scrutinize.mds +3 -0
  132. package/src/assets/agents/simplify.md +3 -0
  133. package/src/assets/agents/skim.mds +4 -0
  134. package/src/assets/agents/synthesize.md +26 -352
  135. package/src/assets/agents/test.md +8 -0
  136. package/src/assets/agents/tracker.md +20 -7
  137. package/src/assets/agents/triage.mds +8 -1
  138. package/src/assets/agents/validate.md +5 -0
  139. package/src/assets/commands/_partials/_compliance.mds +36 -0
  140. package/src/assets/commands/_partials/_decisions.mds +2 -2
  141. package/src/assets/commands/_partials/_engine.mds +8 -8
  142. package/src/assets/commands/_partials/_preamble.mds +3 -3
  143. package/src/assets/commands/_partials/_roster.mds +4 -3
  144. package/src/assets/commands/_partials/_wave.mds +2 -2
  145. package/src/assets/commands/bug-analysis.mds +3 -3
  146. package/src/assets/commands/code-review.mds +11 -8
  147. package/src/assets/commands/debug.mds +24 -2
  148. package/src/assets/commands/dynamic-build.mds +14 -25
  149. package/src/assets/commands/dynamic-plan.mds +4 -3
  150. package/src/assets/commands/dynamic-tickets.mds +5 -4
  151. package/src/assets/commands/explore.mds +1 -1
  152. package/src/assets/commands/implement.mds +89 -125
  153. package/src/assets/commands/plan.mds +12 -7
  154. package/src/assets/commands/release.mds +3 -2
  155. package/src/assets/commands/research.mds +14 -7
  156. package/src/assets/commands/resolve.mds +17 -14
  157. package/src/assets/mds/.mdsroot +4 -0
  158. package/src/assets/mds/git/_ops.mds +383 -0
  159. package/src/assets/mds/git/_ops_pr.mds +201 -0
  160. package/src/assets/mds/git/_ops_steps.mds +147 -0
  161. package/src/assets/mds/git/_pr.mds +39 -44
  162. package/src/assets/mds/git/_references.mds +6 -3
  163. package/src/assets/mds/tracker/_common.mds +5 -5
  164. package/src/assets/mds/tracker/_contract.mds +4 -3
  165. package/src/assets/mds/tracker/_github.mds +60 -14
  166. package/src/assets/mds/tracker/_jira.mds +60 -14
  167. package/src/assets/mds/tracker/_linear.mds +60 -14
  168. package/src/assets/mds/tracker/_steps.mds +10 -9
  169. package/src/assets/rules/behavior.md +6 -0
  170. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +2 -1
  171. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
  172. package/src/assets/skills/agent-references/SKILL.md +33 -0
  173. package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
  174. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
  175. package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
  176. package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
  177. package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
  178. package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
  179. package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
  180. package/src/assets/skills/compliance/SKILL.md +1 -1
  181. package/src/assets/skills/docs-framework/SKILL.md +80 -83
  182. package/src/assets/skills/docs-framework/references/patterns.md +14 -25
  183. package/src/assets/skills/docs-framework/references/violations.md +7 -7
  184. package/src/assets/skills/gap-analysis/SKILL.md +0 -16
  185. package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
  186. package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
@@ -78,9 +78,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
78
78
 
79
79
  The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
80
80
 
81
- **Step 2 — Apply decisions using `devflow:apply-decisions`:**
81
+ **Step 2 — Agents apply the decisions:**
82
82
 
83
- When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions` to scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in downstream agent prompts and reasoning.
83
+ When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
84
84
 
85
85
  Use `DECISIONS_CONTEXT` locally when framing research — prior decisions and pitfalls suggest areas to investigate. Follow `devflow:apply-decisions` to Read full entry bodies on demand. Pass `DECISIONS_CONTEXT` to each Research agent in Phase 4 so their findings can account for relevant decisions.
86
86
 
@@ -145,6 +145,10 @@ Use `FEATURE_KNOWLEDGE` **locally** for research framing. Pass to each Research
145
145
 
146
146
  **Produces:** RESEARCH_PLAN
147
147
 
148
+ **Load `devflow:docs-framework`** — Load via Skill tool: `devflow:docs-framework`, here and not earlier, so a run that stops before this phase never pays for it. If it fails to load, continue with the rules below.
149
+
150
+ Name the research directory by the skill's rules, once: `{topic-slug}` is its topic slug of the research question and `{YYYY-MM-DD_HHMM}` is the local time. When `{worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}` already exists (a same-minute collision), the timestamp takes seconds instead, `YYYY-MM-DD_HHMMSS`. Every path of this run uses that one directory.
151
+
148
152
  Analyze the research question to infer research types needed (min 2, max 5). For each type:
149
153
  - `RESEARCH_TYPE`: `codebase | external | market | competitor | technology`
150
154
  - `RESEARCH_QUESTION`: Focused sub-question for this type
@@ -152,8 +156,6 @@ Analyze the research question to infer research types needed (min 2, max 5). For
152
156
 
153
157
  **Tool availability check**: If WebSearch/WebFetch are unavailable, restrict to `codebase` type only.
154
158
 
155
- Generate topic slug from the research question (kebab-case, lowercase, no articles).
156
-
157
159
  ### Phase 3: Orient (Conditional)
158
160
 
159
161
  **Produces:** ORIENT_OUTPUT
@@ -185,12 +187,17 @@ Each Research agent writes findings to disk at OUTPUT_PATH.
185
187
  **Produces:** RESEARCH_SUMMARY
186
188
  **Requires:** RESEARCH_OUTPUTS
187
189
 
188
- Spawn `Agent(subagent_type="Synthesize")` in `research` mode:
189
- - Reads Research agent outputs from RESEARCH_OUTPUTS paths
190
- - Merges findings with trust-aware aggregation
191
- - Writes `research-summary.md` to the same timestamped directory
190
+ Spawn `Agent(subagent_type="Synthesize")` in `research` mode. It reads the Research agent outputs from the `RESEARCH_OUTPUTS` paths, merges them with trust-aware aggregation, and writes `research-summary.md` into `RESEARCH_BASE_DIR` — the one directory of this run, which only this spawn names:
192
191
 
193
- Output path: `{worktree}/.devflow/docs/research/{topic-slug}/{timestamp}/research-summary.md`
192
+ ```
193
+ Agent(subagent_type="Synthesize", run_in_background=false):
194
+ "Mode: research
195
+ WORKTREE_PATH: {WORKTREE_PATH} (omit if none was provided)
196
+ RESEARCH_OUTPUTS: {the OUTPUT_PATH of every Research agent that wrote its file}
197
+ RESEARCH_BASE_DIR: {worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}
198
+ Merge findings with trust-aware aggregation.
199
+ Output: {worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}/research-summary.md"
200
+ ```
194
201
 
195
202
  ### Phase 6: Present
196
203
 
@@ -22,9 +22,10 @@ Process issues from code review reports: triage every issue through the blast-ra
22
22
 
23
23
  **Produces:** WORKTREES
24
24
 
25
- 1. **Discover resolvable worktrees** using the `devflow:worktree-support` skill discovery algorithm:
26
- - Run `git worktree list --porcelain` → parse, filter (skip protected/detached/mid-rebase), dedup by branch, sort by recent commit
27
- - Invoke the `devflow:worktree-support` skill, then Read its `references/discovery.md` from the skill's base directory for the full 7-step algorithm; the canonical protected branch list stays in the skill
25
+ 1. **Discover resolvable worktrees**:
26
+ - Run `git worktree list --porcelain` and count the worktrees it lists
27
+ - **More than one listed:** Invoke the `devflow:worktree-support` skill, then Read its `references/discovery.md` from the skill's base directory for the full 7-step algorithm; the canonical protected branch list stays in the skill. Parse the list, filter (skip protected/detached/mid-rebase), dedup by branch, sort by recent commit
28
+ - **Exactly one listed:** load neither the skill nor the reference — that worktree is the only candidate, and Step 0b's Git agent validates its branch
28
29
  - Additional filter: must have unresolved reviews (latest review directory has no `resolution-summary.md`)
29
30
  2. **If `--path` flag provided:** use only that worktree, skip discovery
30
31
  **`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop.
@@ -124,9 +125,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
124
125
 
125
126
  The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
126
127
 
127
- **Step 2 — Apply decisions using `devflow:apply-decisions`:**
128
+ **Step 2 — Agents apply the decisions:**
128
129
 
129
- When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions` to scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in downstream agent prompts and reasoning.
130
+ When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
130
131
 
131
132
  This produces a compact index of active ADR/PF entries written at render time — Deprecated/Superseded entries are already stripped. Pass `DECISIONS_CONTEXT` to the Triage agent in Phase 2 and to Code agents in Phase 4.
132
133
 
@@ -205,6 +206,12 @@ Reuse this result for every worktree.
205
206
  Note: `auto` is NOT fail-open — under `auto`, the Git agent probes the repository visibility and treats any error or unrecognised value as PUBLIC (mode STUB). What each value does is decided by the Git agent's publication gate (`references/publication-gate.md` step 2); this partial only resolves the value.
206
207
  Resolve this per worktree — unlike the evidence policy above, the settings line is per worktree root. From the same line: `COMPLIANCE_FRAMEWORKS` is the settings line's `COMPLIANCE` with `generic` written `none`: `off`, `none`, or the framework ids the machine and this repository declare.
207
208
 
209
+ **Set the compliance directive** from `COMPLIANCE_FRAMEWORKS`. `COMPLIANCE_DIRECTIVE` is empty when `COMPLIANCE_FRAMEWORKS` is `off`: a spawn line that holds only `{COMPLIANCE_DIRECTIVE}` is then left out, so a lens-off spawn carries nothing about compliance. Otherwise `COMPLIANCE_DIRECTIVE` is this one line, with `<ids>` replaced by the value of `COMPLIANCE_FRAMEWORKS`:
210
+
211
+ > When the task touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention), invoke Skill(skill="devflow:compliance") and load references/{id}.md only for the ids in <ids> (none: generic controls only); never fabricate guidance for a framework you were not given.
212
+
213
+ Set it per worktree, from that worktree's lens, and pass that worktree's `COMPLIANCE_DIRECTIVE` to each of its Code and Code-fix spawns, on a line of its own.
214
+
208
215
  **Mint `RESOLUTION_TS` once per run:** `date -u +%Y-%m-%dT%H:%M:%SZ`. Every worktree reuses it — the resolution comment's dedup marker is searched per PR, so one run key keeps a retry of this run deduplicated while the next /resolve on the same PR posts. Phase 5 writes it as the summary's `**Date**:` and Phase 9b-2 passes it, so the local file and the posted marker agree.
209
216
 
210
217
  ### Phase 1: Parse Issues
@@ -319,11 +326,10 @@ TASK_ID: resolve-{batch-id}
319
326
  ISSUES: {fix_now_issues_in_batch}
320
327
  SCOPE: {risk_tier_per_issue}
321
328
  PUSH: false
322
- CREATE_PR: false
323
329
  WORKTREE_PATH: {worktree_path} (omit if cwd)
324
330
  DECISIONS_CONTEXT: {decisions_context}
325
331
  FEATURE_KNOWLEDGE: {feature_knowledge}
326
- COMPLIANCE_FRAMEWORKS: {COMPLIANCE_FRAMEWORKS}
332
+ {COMPLIANCE_DIRECTIVE}
327
333
  Fix only the listed pre-classified FIX_NOW issues. Do not re-litigate dispositions.
328
334
  Same-file issues → one commit. Include ## Verification block in report.
329
335
  Regression fix without failing-then-passing regression test → BLOCKED."
@@ -386,18 +392,17 @@ Run build, typecheck, lint, test. Report pass/fail with failure details."
386
392
  1. Extract failure details from Validate agent output
387
393
  2. Increment `validation_retry_count`
388
394
  3. If `validation_retry_count <= 2`:
389
- - Spawn Code agent with fix context:
395
+ - Spawn Code-fix agent with fix context:
390
396
  ```
391
- Agent(subagent_type="Code"):
397
+ Agent(subagent_type="Code-fix"):
392
398
  "OPERATION: validation-fix
393
399
  TASK_ID: resolve-{batch-id}-valfix-{count}
394
400
  VALIDATION_FAILURES: {parsed failures from Validate agent}
395
401
  SCOPE: Fix only the listed failures, no other changes
396
402
  PUSH: false
397
- CREATE_PR: false
398
403
  WORKTREE_PATH: {worktree_path} (omit if cwd)
399
404
  DECISIONS_CONTEXT: {decisions_context}
400
- COMPLIANCE_FRAMEWORKS: {COMPLIANCE_FRAMEWORKS}"
405
+ {COMPLIANCE_DIRECTIVE}"
401
406
  ```
402
407
  - Loop back to re-validate
403
408
  4. If `validation_retry_count > 2`: set VERIFICATION_STATUS = FAILED and exit the validation loop — record a blocking callout in the `## Verification` section (never a silent pass).
@@ -407,7 +412,7 @@ Run build, typecheck, lint, test. Report pass/fail with failure details."
407
412
  Record `## Verification` outcome in resolution-summary.md (runs unconditionally after the validation loop exits):
408
413
  - Rewrite the `## Verification` section with | Command | Result | table + `Final gate: PASS | FAILED after N attempts`
409
414
 
410
- Push all commits to remote once (Code agents and validation-fix Code agents ran with PUSH: false — runs whether the gate PASSED or FAILED, so the branch is always visible before Phase 9 or CI runs):
415
+ Push all commits to remote once (Code and Code-fix agents ran with PUSH: false — runs whether the gate PASSED or FAILED, so the branch is always visible before Phase 9 or CI runs):
411
416
  ```bash
412
417
  git -C {worktree} push
413
418
  ```
@@ -445,7 +450,7 @@ Phase 7 has pushed already, so this is a no-op unless the head moved since. `exi
445
450
  3. **If NO_PR or NO_CI** → skip: "No PR/CI configured, skipping CI validation." Proceed to next phase.
446
451
  4. **If PENDING** and fewer than 3 waits have run → wait again (step 1). After the third wait → report "CI still running — verify manually before merging" and proceed.
447
452
  5. **If INDETERMINATE** and fewer than 3 waits have run → wait again (step 1). After the third wait → report "CI status unknown — verify manually before merging" and proceed.
448
- 6. **If FAILING** and fewer than 2 fixes have run → report the failing checks from the line. Spawn `Agent(subagent_type="Code")` whose prompt opens with `OPERATION: ci-fix`, with `COMPLIANCE_FRAMEWORKS`, `CI_FAILURES`, `DECISIONS_CONTEXT` and `PUSH: false`; `CI_FAILURES` holds the failing-check names from the line and nothing else, because the Code agent fetches the full names and reads the logs itself and this command reads none. After a fix, push with the command above and, if a wait remains, wait again (step 1); a failed push records `TRACEABILITY: DEGRADED (ci push failed)`, reports "CI status unknown — verify manually before merging" and stops waiting. After the second fix still FAILING → report the failing checks and proceed.
453
+ 6. **If FAILING** and fewer than 2 fixes have run → report the failing checks from the line. Spawn `Agent(subagent_type="Code-fix")` whose prompt opens with `OPERATION: ci-fix`, with `COMPLIANCE_DIRECTIVE` (when it is not empty), `CI_FAILURES`, `DECISIONS_CONTEXT` and `PUSH: false`; `CI_FAILURES` holds the failing-check names from the line and nothing else, because the Code-fix agent fetches the full names and reads the logs itself and this command reads none. After a fix, push with the command above and, if a wait remains, wait again (step 1); a failed push records `TRACEABILITY: DEGRADED (ci push failed)`, reports "CI status unknown — verify manually before merging" and stops waiting. After the second fix still FAILING → report the failing checks and proceed.
449
454
  7. **Budget**: at most 3 waits and 2 fixes in all, per worktree. When one is spent, report the current status and proceed.
450
455
  <!-- /PATTERN: ci-status-gate -->
451
456
 
@@ -729,10 +734,10 @@ When the Knowledge agent reports `KB_COMMIT: skipped (detached HEAD)`, the files
729
734
  │
730
735
  ├─ Phase 6: Simplify [Simplify agent]
731
736
  │
732
- ├─ Phase 7: Verification Gate [Validate agent, haiku] + push + Code agent validation-fix loop ≤2
737
+ ├─ Phase 7: Verification Gate [Validate agent, haiku] + push + Code-fix agent validation-fix loop ≤2
733
738
  │
734
739
  ├─ Phase 8: CI Status Gate (conditional — skipped if no fixes or verification FAILED)
735
- │ └─ push, then ci-wait.cjs (inline, one wait ≤ 570 s) → Code agent on FAILING (3 waits + 2 fixes per worktree)
740
+ │ └─ push, then ci-wait.cjs (inline, one wait ≤ 570 s) → Code-fix agent on FAILING (3 waits + 2 fixes per worktree)
736
741
  │
737
742
  ├─ Phase 9: Git agent (manage-debt) — FIX_SEPARATE + TECH_DEBT → backfill Tracked={ISSUE_REF} (or TRACEABILITY: DEGRADED on failure)
738
743
  │ SEQUENTIAL across worktrees
@@ -61,9 +61,9 @@ Attempt to read `{ledger}/.devflow/learning/index.md`.
61
61
 
62
62
  The index is one direct file read, written at render time by `render-decisions.cjs` alongside `decisions.md`/`pitfalls.md` — no `.cjs` script runs here, and the index's own footer names the files that hold each entry's full body.
63
63
 
64
- **Step 2 — Apply decisions using `devflow:apply-decisions`:**
64
+ **Step 2 — Agents apply the decisions:**
65
65
 
66
- When `DECISIONS_CONTEXT` is not `(none)`, follow `devflow:apply-decisions` to scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in downstream agent prompts and reasoning.
66
+ When `DECISIONS_CONTEXT` is not `(none)`, hand it only to the agents whose contract declares it. They follow `devflow:apply-decisions`: scan the index, identify plausibly-relevant entries, Read full entry bodies on demand, and cite verbatim IDs in their reasoning. A host that uses the index in its own thread says so in its own step.
67
67
 
68
68
  Pass `DECISIONS_CONTEXT` to Scrutinize agent — the compact index lists active ADR/PF entries; Scrutinize agent uses `devflow:apply-decisions` to Read full entry bodies on demand. Known pitfalls help identify reintroduced issues, prior decisions help validate architectural consistency. (Simplify agent does not consume decisions — it operates at code-shape level and Scrutinize agent runs after to catch any architectural drift.)
69
69
 
@@ -231,4 +231,105 @@ export function rewriteAgentFrontmatter(content, opts) {
231
231
  const changed = newContent !== content;
232
232
  return Ok({ content: newContent, changed });
233
233
  }
234
+ const WHOLE_SERVER_PREFIX = 'mcp__';
235
+ /** A server name as it appears in a whole-server entry: the charset Claude Code keeps, no `__`. */
236
+ const WHOLE_SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
237
+ /**
238
+ * True when `tool` is a whole-server allowlist entry: `mcp__` and a server name of
239
+ * 1-64 characters from `[A-Za-z0-9_-]` with no `__` (D-WHOLE-SERVER-ENTRY).
240
+ */
241
+ export function isWholeServerTool(tool) {
242
+ if (!tool.startsWith(WHOLE_SERVER_PREFIX))
243
+ return false;
244
+ const name = tool.slice(WHOLE_SERVER_PREFIX.length);
245
+ return WHOLE_SERVER_NAME_RE.test(name) && !name.includes('__');
246
+ }
247
+ /** The single-line inline form `tools: ["A", "B"]`, within a frontmatter body. */
248
+ const INLINE_TOOLS_RE = /^tools:[ \t]*\[([^\r\n]*)\][ \t]*(?=\r?$)/m;
249
+ function toolItems(inner) {
250
+ return inner.split(',').filter(item => item.trim() !== '');
251
+ }
252
+ function unquoteTool(item) {
253
+ return item.trim().replace(/^(["'])(.*)\1$/, '$2');
254
+ }
255
+ /**
256
+ * Replace the whole-server entries of the first frontmatter block's inline `tools:` line
257
+ * with `extraTools`, and touch nothing else.
258
+ *
259
+ * Idempotent: stamping the same list twice yields the same bytes, and stamping a new list
260
+ * replaces the previous one rather than adding to it. Every other entry of the line keeps
261
+ * its original text, and every other byte of the file (other keys, the body, CRLF or LF)
262
+ * is untouched. An empty `extraTools` removes the stamp.
263
+ *
264
+ * Rejects, rather than repairs: an entry that is not a whole-server entry (`invalid-tool`;
265
+ * it would otherwise reach the frontmatter as free text), a file with no inline
266
+ * `tools: [...]` line (`no-tools-line`; a block-list `tools:` is not edited) and a file
267
+ * with no well-formed frontmatter.
268
+ *
269
+ * Pure function — no I/O.
270
+ */
271
+ export function stampAgentTools(content, extraTools) {
272
+ for (const tool of extraTools) {
273
+ if (!isWholeServerTool(tool))
274
+ return Err('invalid-tool');
275
+ }
276
+ const parts = parseFrontmatter(content);
277
+ if (!parts.ok)
278
+ return Err(parts.error);
279
+ const { fmBody, openDelim, closeDelim, afterClose, eol } = parts.value;
280
+ const match = INLINE_TOOLS_RE.exec(fmBody);
281
+ if (!match)
282
+ return Err('no-tools-line');
283
+ const rawItems = toolItems(match[1]);
284
+ const kept = rawItems.filter(item => !isWholeServerTool(unquoteTool(item)));
285
+ const stamped = [...new Set(extraTools)].map(tool => `"${tool}"`);
286
+ // Nothing to remove and nothing to add: the line, whatever its spacing, is already right.
287
+ if (kept.length === rawItems.length && stamped.length === 0)
288
+ return Ok({ content, changed: false });
289
+ // The kept entries keep their own text; only the edges lose the padding that no longer
290
+ // borders a neighbour, so the appended entries land after `, ` and the line stays one shape.
291
+ const keptText = kept
292
+ .map((item, i) => {
293
+ const lead = i === 0 ? item.trimStart() : item;
294
+ return i === kept.length - 1 ? lead.trimEnd() : lead;
295
+ })
296
+ .join(',');
297
+ const inner = [keptText, stamped.join(', ')].filter(text => text !== '').join(', ');
298
+ const line = `tools: [${inner}]`;
299
+ const newBody = fmBody.slice(0, match.index) + line + fmBody.slice(match.index + match[0].length);
300
+ const newContent = openDelim + newBody + eol + closeDelim + afterClose;
301
+ return Ok({ content: newContent, changed: newContent !== content });
302
+ }
303
+ /**
304
+ * The whole-server entries of the first frontmatter block's inline `tools:` line, in
305
+ * order. Ok([]) when the line carries none; an error when there is no line to read.
306
+ * Pure function — no I/O.
307
+ */
308
+ export function readStampedTools(content) {
309
+ const parts = parseFrontmatter(content);
310
+ if (!parts.ok)
311
+ return Err(parts.error);
312
+ const match = INLINE_TOOLS_RE.exec(parts.value.fmBody);
313
+ if (!match)
314
+ return Err('no-tools-line');
315
+ return Ok(toolItems(match[1]).map(unquoteTool).filter(isWholeServerTool));
316
+ }
317
+ /**
318
+ * The text a converge installs for an agent: the variant `source` wearing the whole-server
319
+ * entries the installed copy carries. The stamp is applied at install and by
320
+ * `devflow tracker --servers`, never by a converge, so a converge only CARRIES it. Without
321
+ * the carry, switching an agent between prompt variants would drop its stamp and compare
322
+ * unequal to the installed copy on every run.
323
+ *
324
+ * `source` is returned unchanged when there is nothing safe to carry: either side has no
325
+ * well-formed inline `tools:` line, or the installed copy carries no entries.
326
+ * Pure function — no I/O.
327
+ */
328
+ export function carryStampedTools(source, installed) {
329
+ const carried = readStampedTools(installed);
330
+ if (!carried.ok || carried.value.length === 0)
331
+ return source;
332
+ const stamped = stampAgentTools(source, carried.value);
333
+ return stamped.ok ? stamped.value.content : source;
334
+ }
234
335
  //# sourceMappingURL=agent-frontmatter.js.map
@@ -172,13 +172,15 @@ function collectMissingFragmentWarnings(fnName, activeFrameworks, fragments, omi
172
172
  *
173
173
  * Every framework reference is installed on every machine
174
174
  * (D-COMPLIANCE-INSTALL-ALWAYS), so file presence no longer says which
175
- * frameworks apply. The caller hands the agent `COMPLIANCE_FRAMEWORKS` — the
176
- * `COMPLIANCE` field of the settings line, which folds this machine's selection
177
- * with the repository's `.devflow/project.json` — and those ids alone are in force.
175
+ * frameworks apply. The command names the ids in the agent's prompt
176
+ * (D-COMPLIANCE-INJECT: the compliance directive) — the `COMPLIANCE` field of the
177
+ * settings line, which folds this machine's selection with the repository's
178
+ * `.devflow/project.json` — and those ids alone are in force. No agent takes a
179
+ * named lens input, so the stamp names the prompt, not a variable.
178
180
  */
179
181
  const RUNTIME_SELECTION_LINES = [
180
182
  'Every `references/{id}.md` is installed, so presence decides nothing: the ids you were given',
181
- "(`COMPLIANCE_FRAMEWORKS`, this machine's plus the repository's) are the frameworks in force.",
183
+ "(named in your prompt; this machine's plus the repository's) are the frameworks in force.",
182
184
  'Load `references/{id}.md` for each given id and no other; `none` means generic controls only.',
183
185
  'Apply generic controls always.',
184
186
  '',
@@ -106,6 +106,27 @@ async function readRawManifest(devflowDir) {
106
106
  export async function readMachineFeature(devflowDir, feature) {
107
107
  return isMachineFeatureOn(await readRawManifest(devflowDir), feature);
108
108
  }
109
+ /**
110
+ * Whether a RAW parsed manifest has an opt-in `feature` switched on. Pure.
111
+ *
112
+ * D-BEHAVIOR-OPT-IN: only an explicit boolean `true` is on. A missing key, a
113
+ * non-boolean value, a manifest without a `features` record and anything that is not an
114
+ * object all read as OFF — fail-closed, the mirror of {@link isMachineFeatureOn}. Like
115
+ * that reader it is deliberately not built on readManifest(): a read-only status must
116
+ * not heal-write, and a manifest missing a required field is still a manifest here.
117
+ */
118
+ export function isOptInFeatureOn(rawManifest, feature) {
119
+ if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
120
+ return false;
121
+ return rawManifest.features[feature] === true;
122
+ }
123
+ /**
124
+ * Read an opt-in machine switch from `<devflowDir>/manifest.json`. Read-only (no heal
125
+ * write); an absent, unreadable or malformed manifest reads as OFF.
126
+ */
127
+ export async function readOptInFeature(devflowDir, feature) {
128
+ return isOptInFeatureOn(await readRawManifest(devflowDir), feature);
129
+ }
109
130
  /**
110
131
  * Write the machine-wide switch to `<devflowDir>/manifest.json` (atomic
111
132
  * temp + rename). Refuses with `not-installed` when there is no manifest to
@@ -115,8 +136,8 @@ export async function readMachineFeature(devflowDir, feature) {
115
136
  * D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
116
137
  * nothing. The only bytes such a write could change are `updatedAt`, and that
117
138
  * field means "the manifest's content last changed", so a no-op must not move
118
- * it. This is the one write point of `devflow memory|learning|knowledge
119
- * --enable/--disable`, so the rule holds for all three at once.
139
+ * it. This is the one write point of `devflow memory|learning|knowledge|behavior
140
+ * --enable/--disable`, so the rule holds for all four at once.
120
141
  */
121
142
  export async function writeMachineFeature(devflowDir, feature, enabled) {
122
143
  const raw = await readRawManifest(devflowDir);
@@ -88,6 +88,45 @@ export const FLAG_REGISTRY = [
88
88
  recommended: true,
89
89
  defaultValue: true,
90
90
  },
91
+ {
92
+ // D-SUBAGENT-CACHE-5M: subagents write their prompt cache with the 5-minute TTL
93
+ // while the main session keeps the 1-hour TTL of `prompt-caching-1h` (untouched).
94
+ //
95
+ // Why: the wave-4 token audit (2026-10-10) found 99.998% of subagent cache writes
96
+ // are 1-hour writes, yet fewer than 0.1% of subagent requests idle longer than
97
+ // 5 minutes - a subagent finishes or is resumed long before a 5-minute entry
98
+ // lapses, so the 1-hour premium buys nothing. Modelled effect of 5m: -8.9% on
99
+ // subagent cache-equivalent cost, -7.7% on total.
100
+ //
101
+ // Mechanism (VENDOR BEHAVIOUR, probed 2026-10-11 on Claude Code 2.1.296 in a
102
+ // sandboxed session): with ENABLE_PROMPT_CACHING_1H=true and this setting at
103
+ // "5m", subagent requests wrote only ephemeral_5m cache (36,387 tokens, none at
104
+ // 1h) while the main session wrote 100% 1h (35,209). Precedence of the subagent
105
+ // TTL in the binary, highest first: an env var that forces 5m, env
106
+ // CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, this setting, a per-agent frontmatter
107
+ // `experimental.cacheTtl`, ENABLE_PROMPT_CACHING_1H. The 1-hour TTL is ignored
108
+ // while a subscription is in overage. A Claude Code that predates the setting
109
+ // ignores the key, so subagents then follow ENABLE_PROMPT_CACHING_1H as before.
110
+ // Re-probe when the precedence matters again; this is an observation, not a spec.
111
+ //
112
+ // Rejected: per-agent `experimental.cacheTtl` - it would spread one policy over
113
+ // every agent file; one devflow-managed flag keeps the policy in one place.
114
+ //
115
+ // Delete guard: only "5m" is deleted on disable or uninstall, so a user's own
116
+ // "1h" stays (D-GUARD-SCALAR). Adoption: init seeds this flag OFF when a value
117
+ // that is not "5m" is already on disk (D-ADOPT-RESPECTS-USER, respectUserOwnedSettings).
118
+ id: 'subagent-cache-5m',
119
+ label: 'Subagent 5-min cache',
120
+ description: 'Write subagent prompt-cache entries with the 5-minute TTL; the main session keeps 1 hour',
121
+ hint: 'Subagents rarely idle past 5 min, so 5-min cache writes cost less',
122
+ blurb: '5-min cache TTL for subagents',
123
+ kind: 'boolean',
124
+ target: { type: 'setting', key: 'subagentPromptCacheTtl' },
125
+ onPayload: '5m',
126
+ settingDeleteGuard: '5m',
127
+ recommended: true,
128
+ defaultValue: true,
129
+ },
91
130
  {
92
131
  id: 'show-turn-duration',
93
132
  label: 'Show turn duration',
@@ -969,6 +1008,78 @@ export function settingHoldsManagedShape(settingsJson, flagId) {
969
1008
  return false;
970
1009
  }
971
1010
  }
1011
+ /**
1012
+ * D-ADOPT-RESPECTS-USER predicate: the mirror of `settingHoldsManagedShape`.
1013
+ *
1014
+ * Returns true when the settings.json string carries a value at the guarded flag's
1015
+ * target key that is NOT the flag's managed shape - a value the user (or another
1016
+ * tool) owns, which enabling the flag would overwrite. Presence is decided with
1017
+ * `hasOwnProperty`, so an explicit `null` counts as a value; the shape decision is
1018
+ * delegated to `settingValueHoldsManagedShape`, never compared here.
1019
+ *
1020
+ * Returns false when:
1021
+ * - settingsJson is malformed
1022
+ * - root is not a plain object
1023
+ * - flagId is not in the registry
1024
+ * - the flag has no settingDeleteGuard, or is not a setting-target boolean
1025
+ * - the key is absent
1026
+ * - the on-disk value holds the managed shape
1027
+ */
1028
+ export function settingHoldsUserValue(settingsJson, flagId) {
1029
+ try {
1030
+ const parsed = JSON.parse(settingsJson);
1031
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
1032
+ return false;
1033
+ const flag = FLAG_REGISTRY_MAP.get(flagId);
1034
+ if (!flag || flag.kind !== 'boolean' || flag.target.type !== 'setting')
1035
+ return false;
1036
+ if (flag.settingDeleteGuard === undefined)
1037
+ return false;
1038
+ const root = parsed;
1039
+ if (!Object.prototype.hasOwnProperty.call(root, flag.target.key))
1040
+ return false;
1041
+ return !settingValueHoldsManagedShape(flag, root[flag.target.key]);
1042
+ }
1043
+ catch {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ /**
1048
+ * D-ADOPT-RESPECTS-USER: seed a guarded setting flag OFF when adopting it ON would
1049
+ * overwrite a value the user already owns.
1050
+ *
1051
+ * A flag the manifest does not record is adopted at its `defaultValue`. For a flag whose
1052
+ * target is a settings.json key with a `settingDeleteGuard`, adopting ON writes its
1053
+ * managed payload over whatever is on disk (the guard protects deletion, not writes).
1054
+ * When the key already holds a value that is not the managed shape - a user's own `"1h"`
1055
+ * under `subagentPromptCacheTtl` - that write would silently replace a deliberate choice.
1056
+ * Such a flag is set `false` instead; with the record then claiming `false`, the converge
1057
+ * pipeline neither writes nor deletes the user's value.
1058
+ *
1059
+ * Scope: only flags ABSENT from `manifestFlags` (or all of them on a fresh install, when
1060
+ * it is null). A flag the manifest records keeps its value in `flags`, so an explicit
1061
+ * enable still wins. `devflow init --reset` hands this an empty settings snapshot
1062
+ * (resolveResetGatedInputs), so nothing is detected and the factory default stands. The rule is generic over guarded
1063
+ * setting flags: no flag id is special-cased, and `settingHoldsUserValue` is the one place
1064
+ * that decides "the disk holds something that is not ours". For a flag whose default is
1065
+ * already OFF (suppress-attribution) it changes nothing.
1066
+ *
1067
+ * Two callers apply the same rule: the init seed, and the flags TUI's starting rows (an
1068
+ * upgrade that has not run init yet would otherwise offer a Save that overwrites the value).
1069
+ *
1070
+ * Pure function - the settings snapshot is read by the caller and passed in.
1071
+ */
1072
+ export function respectUserOwnedSettings(flags, manifestFlags, settingsSnapshot) {
1073
+ const result = { ...flags };
1074
+ for (const flag of FLAG_REGISTRY) {
1075
+ const adoptedAtDefault = manifestFlags === null || !(flag.id in manifestFlags);
1076
+ if (!adoptedAtDefault)
1077
+ continue;
1078
+ if (settingHoldsUserValue(settingsSnapshot, flag.id))
1079
+ result[flag.id] = false;
1080
+ }
1081
+ return result;
1082
+ }
972
1083
  /**
973
1084
  * D-ATTR-GUARD: returns true when it is safe to delete a flag's target key from
974
1085
  * settings. For flags with a settingDeleteGuard the key is only deleted when the
@@ -62,12 +62,15 @@ function parseManifestFlags(features, knownFlags) {
62
62
  * - features.knownFlags stripped from result (folded into FlagsRecord key-presence)
63
63
  * - features.proxy absent → false
64
64
  * - features.compliance absent/malformed → {enabled:false, frameworks:[]}
65
+ * - features.behavior absent → false; malformed → false, healed on disk (D-BEHAVIOR-OPT-IN)
65
66
  * - features.tracker absent/malformed → {provider:'github'}
67
+ * - features.tracker.servers absent → the default list; invalid → the default list, reported
68
+ * through `opts.onWarning` when the caller supplies one (this reader has no output channel)
66
69
  *
67
70
  * D39: heal-write failure returns the migrated in-memory manifest (not null).
68
71
  * The on-disk format remains unhealed; next read triggers another attempt.
69
72
  */
70
- export async function readManifest(devflowDir) {
73
+ export async function readManifest(devflowDir, opts) {
71
74
  const manifestPath = path.join(devflowDir, 'manifest.json');
72
75
  try {
73
76
  const content = await fs.readFile(manifestPath, 'utf-8');
@@ -129,7 +132,10 @@ export async function readManifest(devflowDir) {
129
132
  features.decisions !== undefined ||
130
133
  flagsWereLegacy ||
131
134
  features.knownFlags !== undefined ||
132
- features.viewMode !== undefined;
135
+ features.viewMode !== undefined ||
136
+ // D-BEHAVIOR-OPT-IN: a malformed switch (present, not a boolean) is rewritten as
137
+ // the false it reads as; an absent one is not a defect and owes no write.
138
+ (features.behavior !== undefined && typeof features.behavior !== 'boolean');
133
139
  const SECURITY_MODES = ['none', 'user', 'managed'];
134
140
  const manifest = {
135
141
  version: data.version,
@@ -158,12 +164,14 @@ export async function readManifest(devflowDir) {
158
164
  proxy: typeof features.proxy === 'boolean' ? features.proxy : false,
159
165
  // Self-heal: absent/malformed compliance → {enabled:false, frameworks:[]}
160
166
  compliance: normalizeComplianceFeature(features.compliance),
167
+ // D-BEHAVIOR-OPT-IN: absent/malformed → false (opt-in; the inverse of knowledge/learning).
168
+ behavior: features.behavior === true,
161
169
  // Self-heal: absent/malformed/unknown tracker → {provider:'github'} (AC-3.21).
162
170
  // Deliberately NOT in the hard-null set above — see the field's doc comment.
163
171
  // Healing here is silent and emits no DEGRADED: that is the correct
164
172
  // self-heal behaviour, and a different condition from a per-repo config
165
173
  // value outside the domain (which does emit `unknown tracker provider`).
166
- tracker: normalizeTrackerFeature(features.tracker),
174
+ tracker: normalizeTrackerFeature(features.tracker, opts?.onWarning),
167
175
  },
168
176
  installedAt: data.installedAt,
169
177
  updatedAt: data.updatedAt,