devflow-kit 3.4.0 → 3.6.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 (221) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +12 -8
  3. package/dist/agents/code.md +39 -182
  4. package/dist/agents/{diagnose.md → debug.md} +4 -4
  5. package/dist/agents/design.md +0 -1
  6. package/dist/agents/fix.md +165 -0
  7. package/dist/agents/git.md +31 -693
  8. package/dist/agents/{scrutinize.md → harden.md} +9 -6
  9. package/dist/agents/knowledge.md +8 -2
  10. package/dist/agents/refactor.md +133 -0
  11. package/dist/agents/research.md +6 -0
  12. package/dist/agents/review.md +0 -4
  13. package/dist/agents/skim.md +4 -0
  14. package/dist/agents/triage.md +8 -1
  15. package/dist/cli/agents-view/render.js +1 -1
  16. package/dist/cli/commands/behavior-prompts.js +77 -0
  17. package/dist/cli/commands/behavior.js +123 -0
  18. package/dist/cli/commands/flags.js +8 -2
  19. package/dist/cli/commands/init-seed.js +7 -3
  20. package/dist/cli/commands/init.js +75 -8
  21. package/dist/cli/commands/learning.js +12 -12
  22. package/dist/cli/commands/legacy-hooks.js +1 -1
  23. package/dist/cli/commands/rules.js +16 -0
  24. package/dist/cli/commands/tracker.js +112 -15
  25. package/dist/cli/commands/uninstall.js +111 -5
  26. package/dist/cli.js +2 -0
  27. package/dist/commands/bug-analysis.md +22 -18
  28. package/dist/commands/code-review.md +25 -29
  29. package/dist/commands/debug.md +36 -15
  30. package/dist/commands/dynamic-build.md +64 -78
  31. package/dist/commands/dynamic-plan.md +18 -14
  32. package/dist/commands/dynamic-profile.md +8 -6
  33. package/dist/commands/dynamic-tickets.md +19 -15
  34. package/dist/commands/explore.md +14 -14
  35. package/dist/commands/implement.md +122 -225
  36. package/dist/commands/plan.md +32 -47
  37. package/dist/commands/release.md +12 -7
  38. package/dist/commands/research.md +26 -15
  39. package/dist/commands/resolve.md +53 -92
  40. package/dist/commands/self-review.md +42 -42
  41. package/dist/core/agent-frontmatter.js +101 -0
  42. package/dist/core/agent-models.js +47 -11
  43. package/dist/core/compliance-compose.js +6 -4
  44. package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
  45. package/dist/core/feature-switch.js +23 -2
  46. package/dist/core/flags.js +111 -0
  47. package/dist/core/learning-queue-cleanup.js +1 -1
  48. package/dist/core/manifest.js +11 -3
  49. package/dist/core/mds-variants.js +68 -26
  50. package/dist/core/migrations.js +63 -44
  51. package/dist/core/plugins.js +49 -70
  52. package/dist/core/project-paths.js +3 -7
  53. package/dist/core/proxy-log.js +1 -1
  54. package/dist/core/proxy-state.js +1 -1
  55. package/dist/core/tracker.js +120 -2
  56. package/dist/hud/codex-usage.js +247 -0
  57. package/dist/hud/components/codex-quota.js +37 -0
  58. package/dist/hud/components/usage-quota.js +1 -1
  59. package/dist/hud/config.js +1 -0
  60. package/dist/hud/index.js +16 -3
  61. package/dist/hud/render.js +3 -1
  62. package/dist/learning-off/agents/code.md +37 -180
  63. package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
  64. package/dist/learning-off/agents/design.md +0 -1
  65. package/dist/learning-off/agents/fix.md +161 -0
  66. package/dist/learning-off/agents/{scrutinize.md → harden.md} +9 -6
  67. package/dist/learning-off/agents/knowledge.md +8 -2
  68. package/dist/learning-off/agents/refactor.md +129 -0
  69. package/dist/learning-off/agents/research.md +6 -0
  70. package/dist/learning-off/agents/review.md +0 -4
  71. package/dist/learning-off/agents/skim.md +4 -0
  72. package/dist/learning-off/agents/triage.md +8 -1
  73. package/dist/learning-off/commands/bug-analysis.md +12 -12
  74. package/dist/learning-off/commands/code-review.md +14 -22
  75. package/dist/learning-off/commands/debug.md +24 -8
  76. package/dist/learning-off/commands/dynamic-build.md +58 -74
  77. package/dist/learning-off/commands/dynamic-plan.md +12 -10
  78. package/dist/learning-off/commands/dynamic-profile.md +2 -2
  79. package/dist/learning-off/commands/dynamic-tickets.md +13 -11
  80. package/dist/learning-off/commands/explore.md +3 -7
  81. package/dist/learning-off/commands/implement.md +110 -217
  82. package/dist/learning-off/commands/plan.md +21 -39
  83. package/dist/learning-off/commands/release.md +7 -4
  84. package/dist/learning-off/commands/research.md +16 -9
  85. package/dist/learning-off/commands/resolve.md +42 -85
  86. package/dist/learning-off/commands/self-review.md +30 -34
  87. package/dist/skills/git/references/learn-conventions.md +27 -3
  88. package/dist/skills/git/references/pr/check-ci-status.md +19 -0
  89. package/dist/skills/git/references/pr/check-merge-readiness.md +21 -0
  90. package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
  91. package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
  92. package/dist/skills/git/references/pr/post-review-summary.md +20 -0
  93. package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
  94. package/dist/skills/git/references/pr/update-pr-evidence.md +17 -6
  95. package/dist/skills/git/references/pr/validate-branch.md +26 -0
  96. package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
  97. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
  98. package/dist/skills/git/references/tracker/github/create-release.md +30 -1
  99. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
  100. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
  101. package/dist/skills/git/references/tracker/github/fetch-issue.md +36 -1
  102. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +47 -1
  103. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +25 -0
  104. package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
  105. package/dist/skills/git/references/tracker/github/post-wave-report.md +15 -1
  106. package/dist/skills/git/references/tracker/github/setup-task.md +41 -1
  107. package/dist/skills/git/references/tracker/jira/associate-release.md +24 -0
  108. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
  109. package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
  110. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
  111. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
  112. package/dist/skills/git/references/tracker/jira/fetch-issue.md +36 -1
  113. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +47 -1
  114. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +25 -0
  115. package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
  116. package/dist/skills/git/references/tracker/jira/post-wave-report.md +15 -1
  117. package/dist/skills/git/references/tracker/jira/setup-task.md +41 -1
  118. package/dist/skills/git/references/tracker/linear/associate-release.md +24 -0
  119. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
  120. package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
  121. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
  122. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
  123. package/dist/skills/git/references/tracker/linear/fetch-issue.md +36 -1
  124. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +47 -1
  125. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +25 -0
  126. package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
  127. package/dist/skills/git/references/tracker/linear/post-wave-report.md +15 -1
  128. package/dist/skills/git/references/tracker/linear/setup-task.md +41 -1
  129. package/dist/targets/claude-code/agent-type-owner.js +164 -0
  130. package/dist/targets/claude-code/behavior-install.js +111 -0
  131. package/dist/targets/claude-code/installer.js +177 -77
  132. package/dist/targets/claude-code/learning-install.js +9 -1
  133. package/dist/targets/claude-code/tracker-install.js +33 -16
  134. package/dist/targets/claude-code/tracker-servers.js +107 -0
  135. package/package.json +4 -3
  136. package/src/assets/agents/_partials/_code.mds +109 -0
  137. package/src/assets/agents/code.mds +18 -222
  138. package/src/assets/agents/{learning.md → curate.md} +22 -72
  139. package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
  140. package/src/assets/agents/design.mds +0 -1
  141. package/src/assets/agents/evaluate.md +11 -5
  142. package/src/assets/agents/explore.md +58 -0
  143. package/src/assets/agents/fix.mds +113 -0
  144. package/src/assets/agents/git.mds +31 -693
  145. package/src/assets/agents/{scrutinize.mds → harden.mds} +9 -6
  146. package/src/assets/agents/knowledge.mds +8 -2
  147. package/src/assets/agents/{simplify.md → polish.md} +7 -4
  148. package/src/assets/agents/refactor.mds +81 -0
  149. package/src/assets/agents/research.mds +6 -0
  150. package/src/assets/agents/review.mds +0 -4
  151. package/src/assets/agents/skim.mds +4 -0
  152. package/src/assets/agents/synthesize.md +26 -352
  153. package/src/assets/agents/test.md +10 -2
  154. package/src/assets/agents/tracker.md +20 -7
  155. package/src/assets/agents/triage.mds +8 -1
  156. package/src/assets/agents/validate.md +5 -0
  157. package/src/assets/commands/_partials/_compliance.mds +36 -0
  158. package/src/assets/commands/_partials/_decisions.mds +26 -6
  159. package/src/assets/commands/_partials/_engine.mds +17 -17
  160. package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
  161. package/src/assets/commands/_partials/_knowledge.mds +14 -9
  162. package/src/assets/commands/_partials/_preamble.mds +5 -3
  163. package/src/assets/commands/_partials/_roster.mds +6 -5
  164. package/src/assets/commands/_partials/_wave.mds +2 -2
  165. package/src/assets/commands/bug-analysis.mds +10 -10
  166. package/src/assets/commands/code-review.mds +12 -22
  167. package/src/assets/commands/debug.mds +24 -2
  168. package/src/assets/commands/dynamic-build.mds +34 -51
  169. package/src/assets/commands/dynamic-plan.mds +4 -3
  170. package/src/assets/commands/dynamic-tickets.mds +5 -4
  171. package/src/assets/commands/explore.mds +1 -1
  172. package/src/assets/commands/implement.mds +108 -201
  173. package/src/assets/commands/plan.mds +19 -44
  174. package/src/assets/commands/release.mds +8 -3
  175. package/src/assets/commands/research.mds +14 -7
  176. package/src/assets/commands/resolve.mds +37 -82
  177. package/src/assets/commands/self-review.mds +27 -27
  178. package/src/assets/mds/.mdsroot +4 -0
  179. package/src/assets/mds/git/_ops.mds +383 -0
  180. package/src/assets/mds/git/_ops_pr.mds +201 -0
  181. package/src/assets/mds/git/_ops_steps.mds +147 -0
  182. package/src/assets/mds/git/_pr.mds +45 -50
  183. package/src/assets/mds/git/_references.mds +6 -3
  184. package/src/assets/mds/tracker/_common.mds +5 -5
  185. package/src/assets/mds/tracker/_contract.mds +4 -3
  186. package/src/assets/mds/tracker/_github.mds +60 -14
  187. package/src/assets/mds/tracker/_jira.mds +60 -14
  188. package/src/assets/mds/tracker/_linear.mds +60 -14
  189. package/src/assets/mds/tracker/_steps.mds +10 -9
  190. package/src/assets/rules/behavior.md +6 -0
  191. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -3
  192. package/src/assets/scripts/hooks/capture-prompt +1 -1
  193. package/src/assets/scripts/hooks/capture-question +1 -1
  194. package/src/assets/scripts/hooks/capture-turn +1 -1
  195. package/src/assets/scripts/hooks/json-helper.cjs +2 -2
  196. package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
  197. package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -10
  198. package/src/assets/scripts/hooks/preamble +1 -1
  199. package/src/assets/scripts/hooks/session-start-context +58 -45
  200. package/src/assets/scripts/verify-evidence.cjs +726 -9
  201. package/src/assets/skills/agent-references/SKILL.md +37 -0
  202. package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
  203. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
  204. package/src/assets/skills/agent-references/references/curate/maintain.md +71 -0
  205. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
  206. package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
  207. package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
  208. package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
  209. package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
  210. package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
  211. package/src/assets/skills/apply-decisions/SKILL.md +6 -4
  212. package/src/assets/skills/compliance/SKILL.md +1 -1
  213. package/src/assets/skills/docs-framework/SKILL.md +81 -84
  214. package/src/assets/skills/docs-framework/references/patterns.md +14 -25
  215. package/src/assets/skills/docs-framework/references/violations.md +7 -7
  216. package/src/assets/skills/gap-analysis/SKILL.md +0 -16
  217. package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
  218. package/src/assets/skills/qa/SKILL.md +1 -1
  219. package/src/assets/skills/quality-gates/SKILL.md +7 -7
  220. package/src/assets/skills/quality-gates/references/report-template.md +7 -7
  221. package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
@@ -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
@@ -142,38 +142,56 @@ export function validateWorkerValue(field, value) {
142
142
  // Key migration
143
143
  // ---------------------------------------------------------------------------
144
144
  /**
145
- * Maps old agent keys (form A slugs) to their canonical replacement keys.
145
+ * Maps every retired agent key to the live slug that replaces it. Two renames feed it:
146
146
  *
147
- * 13 agents renamed from noun form (coder, reviewer, …) to action-verb form
148
- * (code, review, …). The canonicalise-agent-keys-v1 migration in migrations.ts
149
- * calls canonicaliseAgentKeys() to rewrite ~/.devflow/agent-models.json on the
150
- * user's first init after upgrading.
147
+ * - The 2.0.0 action-verb rename (coder, reviewer, …) and the 1.x names that rename
148
+ * retired (scrutinizer, simplifier, bug-analyzer).
149
+ * - The rename of six agents to the names that state their job (code-fix, code-edit,
150
+ * scrutinize, simplify, diagnose, learning).
151
+ *
152
+ * SINGLE HOP: every value is a live slug and no value is itself a key. A 1.x name
153
+ * therefore points straight at its final slug (scrutinizer → harden, never
154
+ * scrutinizer → scrutinize → harden), so one pass of canonicaliseAgentKeys() lands
155
+ * every key. The two canonicalise-agent-keys migrations in migrations.ts call it to
156
+ * rewrite ~/.devflow/agent-models.json; readAgentMapping() calls it on every read.
151
157
  *
152
158
  * PROTOTYPE SAFETY: Object.create(null) prevents prototype-pollution.
153
159
  * Every lookup MUST use Object.hasOwn — never key in LEGACY_AGENT_KEYS or
154
160
  * direct bracket access. `--set constructor` would otherwise return a function
155
161
  * that flows into path.join (the object-literal lookup on argv trap).
156
162
  *
163
+ * ORDER: entries are declared oldest first. canonicaliseAgentKeys() reads that order
164
+ * when two keys meet at one slug (scrutinizer and scrutinize both mean harden): the
165
+ * entry declared later is the more recent name and wins.
166
+ *
157
167
  * DORMANCY: entries here are stored whether or not the key rename has been
158
168
  * applied to the file yet. canonicaliseAgentKeys() is idempotent, so
159
169
  * double-execution from concurrent sessions is a harmless no-op — the second
160
170
  * run finds the file already migrated and makes zero writes.
161
171
  */
162
172
  export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
163
- // Phase 4 — agent-action-verbs rename (old slug → new slug)
173
+ // 1.x names (noun form), retargeted to the live slug of their final rename.
174
+ 'scrutinizer': 'harden',
175
+ 'simplifier': 'polish',
176
+ 'bug-analyzer': 'debug',
177
+ // 2.0.0 action-verb rename (old slug → new slug).
164
178
  'coder': 'code',
165
179
  'designer': 'design',
166
180
  'evaluator': 'evaluate',
167
181
  'researcher': 'research',
168
182
  'reviewer': 'review',
169
- 'scrutinizer': 'scrutinize',
170
- 'simplifier': 'simplify',
171
183
  'skimmer': 'skim',
172
184
  'synthesizer': 'synthesize',
173
185
  'tester': 'test',
174
186
  'triager': 'triage',
175
187
  'validator': 'validate',
176
- 'bug-analyzer': 'diagnose',
188
+ // Rename of six agents to the names that state their job.
189
+ 'code-fix': 'fix',
190
+ 'code-edit': 'refactor',
191
+ 'scrutinize': 'harden',
192
+ 'simplify': 'polish',
193
+ 'diagnose': 'debug',
194
+ 'learning': 'curate',
177
195
  });
178
196
  /**
179
197
  * Canonicalise agent keys in a raw agents map by applying LEGACY_AGENT_KEYS.
@@ -184,6 +202,10 @@ export const LEGACY_AGENT_KEYS = Object.assign(Object.create(null), {
184
202
  *
185
203
  * Collision detection is against the ORIGINAL map so results are order-independent.
186
204
  *
205
+ * Two legacy keys that name one agent (scrutinizer and scrutinize both mean harden) meet
206
+ * at one slug. The key declared later in LEGACY_AGENT_KEYS, the more recent name, wins;
207
+ * the other is dropped. The pick reads the declaration order, never the file order.
208
+ *
187
209
  * The function is pure and idempotent: calling it twice on the same input
188
210
  * produces the same result as calling it once (idempotent under concurrent execution).
189
211
  *
@@ -208,6 +230,19 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
208
230
  }
209
231
  // Snapshot the original keys for collision detection (order-independent result).
210
232
  const originalKeys = new Set(Object.keys(rawAgents));
233
+ // When several legacy keys in the file name one agent, the one declared latest in
234
+ // LEGACY_AGENT_KEYS is the one that is renamed; the others are dropped below.
235
+ const declared = Object.keys(LEGACY_AGENT_KEYS);
236
+ const winnerByTarget = new Map();
237
+ for (const oldKey of originalKeys) {
238
+ if (oldKey === '__proto__' || !Object.hasOwn(LEGACY_AGENT_KEYS, oldKey))
239
+ continue;
240
+ const target = LEGACY_AGENT_KEYS[oldKey];
241
+ const held = winnerByTarget.get(target);
242
+ if (held === undefined || declared.indexOf(oldKey) > declared.indexOf(held)) {
243
+ winnerByTarget.set(target, oldKey);
244
+ }
245
+ }
211
246
  const result = { ...rawAgents };
212
247
  let didMutate = false;
213
248
  for (const oldKey of originalKeys) {
@@ -237,8 +272,9 @@ export function canonicaliseAgentKeys(rawAgents, onWarning) {
237
272
  didMutate = true;
238
273
  continue;
239
274
  }
240
- if (originalKeys.has(newKey)) {
241
- // Collision: new (canonical) key already present → new wins, old dropped.
275
+ if (originalKeys.has(newKey) || winnerByTarget.get(newKey) !== oldKey) {
276
+ // Collision: new (canonical) key already present, or a more recent legacy key
277
+ // names the same agent → that value wins, old dropped.
242
278
  // Warning is surfaced by the caller's structured dropped-block (e.g. migrations.ts)
243
279
  // to avoid printing the same collision twice in two different vocabularies (F8).
244
280
  dropped.push(oldKey);
@@ -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
  '',
@@ -1,17 +1,17 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getDevFlowDirectory } from '../targets/claude-code/claude-paths.js';
4
- import { getLearningTuningConfigPath } from './project-paths.js';
4
+ import { getCurateTuningConfigPath } from './project-paths.js';
5
5
  const VALID_MODELS = new Set(['opus', 'sonnet', 'haiku']);
6
6
  function isValidModel(value) {
7
7
  return typeof value === 'string' && VALID_MODELS.has(value);
8
8
  }
9
9
  /**
10
- * D-LEARNING-SHIPPED-ROW: the default model is the model the Learning agent ships
11
- * with, the `model:` line of src/assets/agents/learning.md. With no layer
10
+ * D-LEARNING-SHIPPED-ROW: the default model is the model the Curate agent ships
11
+ * with, the `model:` line of src/assets/agents/curate.md. With no layer
12
12
  * configuring a model, the spawn directive names none and that line decides, so a
13
13
  * different default here would describe a run that never happens.
14
- * tests/decisions/config.test.ts holds this value equal to the Learning row of the
14
+ * tests/decisions/config.test.ts holds this value equal to the Curate row of the
15
15
  * agent-config table, which a change of the shipped tier edits with the frontmatter.
16
16
  */
17
17
  const DEFAULTS = {
@@ -19,12 +19,12 @@ const DEFAULTS = {
19
19
  debug: false,
20
20
  };
21
21
  /**
22
- * Apply a single JSON config layer onto a LearningTuningConfig, returning a new object.
22
+ * Apply a single JSON config layer onto a CurateTuningConfig, returning a new object.
23
23
  * Skips fields with wrong types. Swallows parse errors — callers see defaults.
24
24
  * Unknown fields (e.g. an old config still on disk with extra knobs) are
25
25
  * silently ignored, not an error.
26
26
  */
27
- export function applyLearningTuningConfigLayer(config, json) {
27
+ export function applyCurateTuningConfigLayer(config, json) {
28
28
  try {
29
29
  const parsed = JSON.parse(json);
30
30
  // Object-shape guard mirrors coerceConfig in feature-config.ts: a JSON
@@ -34,7 +34,7 @@ export function applyLearningTuningConfigLayer(config, json) {
34
34
  }
35
35
  const raw = parsed;
36
36
  return {
37
- // Only accept a model value that belongs to the closed LearningModelAlias domain.
37
+ // Only accept a model value that belongs to the closed CurateModelAlias domain.
38
38
  model: isValidModel(raw.model) ? raw.model : config.model,
39
39
  debug: typeof raw.debug === 'boolean' ? raw.debug : config.debug,
40
40
  };
@@ -54,13 +54,13 @@ function readConfigFile(filePath) {
54
54
  catch (err) {
55
55
  if (err.code !== 'ENOENT') {
56
56
  // Warn but don't crash — callers fall back to defaults.
57
- console.warn(`[learning-tuning-config] warning: could not read ${filePath}: ${err.message}`);
57
+ console.warn(`[curate-tuning-config] warning: could not read ${filePath}: ${err.message}`);
58
58
  }
59
59
  return null;
60
60
  }
61
61
  }
62
62
  /**
63
- * Load and merge learning agent tuning configuration.
63
+ * Load and merge curate agent tuning configuration.
64
64
  *
65
65
  * Priority (highest wins): project config → global config → defaults.
66
66
  *
@@ -71,18 +71,18 @@ function readConfigFile(filePath) {
71
71
  *
72
72
  * Invalid JSON in either file is silently ignored and treated as absent.
73
73
  */
74
- export function loadLearningTuningConfig(ledgerRoot) {
74
+ export function loadCurateTuningConfig(ledgerRoot) {
75
75
  const globalConfigPath = path.join(getDevFlowDirectory(), 'learning.json');
76
- const projectConfigPath = getLearningTuningConfigPath(ledgerRoot);
76
+ const projectConfigPath = getCurateTuningConfigPath(ledgerRoot);
77
77
  let config = { ...DEFAULTS };
78
78
  const globalJson = readConfigFile(globalConfigPath);
79
79
  if (globalJson !== null) {
80
- config = applyLearningTuningConfigLayer(config, globalJson);
80
+ config = applyCurateTuningConfigLayer(config, globalJson);
81
81
  }
82
82
  const projectJson = readConfigFile(projectConfigPath);
83
83
  if (projectJson !== null) {
84
- config = applyLearningTuningConfigLayer(config, projectJson);
84
+ config = applyCurateTuningConfigLayer(config, projectJson);
85
85
  }
86
86
  return config;
87
87
  }
88
- //# sourceMappingURL=learning-tuning-config.js.map
88
+ //# sourceMappingURL=curate-tuning-config.js.map
@@ -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
@@ -11,7 +11,7 @@ import { drainQueueFiles } from './queue-drain.js';
11
11
  /**
12
12
  * Drain the learning (decisions-detection) pending-turns queue, its claimed
13
13
  * batch and the claim's owner file so stale turns don't process later — used by
14
- * both `--clear` and `--disable`. A mid-run Learning agent whose claimed batch
14
+ * both `--clear` and `--disable`. A mid-run Curate agent whose claimed batch
15
15
  * vanishes aborts without changes, which is the desired outcome in both cases.
16
16
  * Refused, deleting nothing, when `.devflow` or `.devflow/learning` under `gitRoot`
17
17
  * is a symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; other errors propagate.
@@ -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,