devflow-kit 3.3.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 (220) hide show
  1. package/CHANGELOG.md +47 -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 +187 -0
  6. package/{src/assets → dist}/agents/design.md +1 -2
  7. package/{src/assets → dist}/agents/diagnose.md +1 -2
  8. package/dist/agents/git.md +33 -722
  9. package/{src/assets → dist}/agents/knowledge.md +12 -5
  10. package/{src/assets → dist}/agents/research.md +8 -2
  11. package/{src/assets → dist}/agents/review.md +8 -11
  12. package/{src/assets → dist}/agents/scrutinize.md +4 -1
  13. package/dist/agents/skim.md +152 -0
  14. package/{src/assets → dist}/agents/triage.md +9 -2
  15. package/dist/cli/commands/behavior-prompts.js +77 -0
  16. package/dist/cli/commands/behavior.js +123 -0
  17. package/dist/cli/commands/flags.js +8 -2
  18. package/dist/cli/commands/init-seed.js +7 -3
  19. package/dist/cli/commands/init.js +134 -5
  20. package/dist/cli/commands/learning.js +38 -3
  21. package/dist/cli/commands/rules.js +16 -0
  22. package/dist/cli/commands/tracker.js +112 -15
  23. package/dist/cli/commands/uninstall.js +42 -1
  24. package/dist/cli.js +2 -0
  25. package/dist/commands/bug-analysis.md +35 -13
  26. package/dist/commands/code-review.md +155 -69
  27. package/dist/commands/debug.md +39 -16
  28. package/dist/commands/dynamic-build.md +61 -72
  29. package/dist/commands/dynamic-plan.md +39 -25
  30. package/dist/commands/dynamic-profile.md +28 -14
  31. package/dist/commands/dynamic-tickets.md +38 -22
  32. package/dist/commands/explore.md +18 -16
  33. package/dist/commands/implement.md +121 -150
  34. package/dist/commands/plan.md +51 -31
  35. package/dist/commands/release.md +72 -6
  36. package/dist/commands/research.md +49 -20
  37. package/dist/commands/resolve.md +54 -46
  38. package/dist/commands/self-review.md +38 -25
  39. package/dist/core/agent-frontmatter.js +101 -0
  40. package/dist/core/agent-models.js +43 -0
  41. package/dist/core/assets.js +55 -10
  42. package/dist/core/claude-md-audit.js +190 -0
  43. package/dist/core/compliance-compose.js +6 -4
  44. package/dist/core/feature-switch.js +41 -1
  45. package/dist/core/flags.js +139 -0
  46. package/dist/core/fs-atomic.js +8 -3
  47. package/dist/core/learning-variants.js +213 -0
  48. package/dist/core/manifest.js +73 -3
  49. package/dist/core/mds-variants.js +98 -19
  50. package/dist/core/plugins.js +111 -72
  51. package/dist/core/project-paths.js +0 -4
  52. package/dist/core/proxy-log.js +1 -1
  53. package/dist/core/proxy-state.js +1 -1
  54. package/dist/core/tracker.js +120 -2
  55. package/dist/learning-off/agents/code-edit.md +129 -0
  56. package/dist/learning-off/agents/code-fix.md +161 -0
  57. package/dist/learning-off/agents/code.md +183 -0
  58. package/dist/learning-off/agents/design.md +118 -0
  59. package/dist/learning-off/agents/diagnose.md +210 -0
  60. package/dist/learning-off/agents/knowledge.md +96 -0
  61. package/dist/learning-off/agents/research.md +155 -0
  62. package/dist/learning-off/agents/review.md +224 -0
  63. package/dist/learning-off/agents/scrutinize.md +120 -0
  64. package/{src/assets → dist/learning-off}/agents/skim.md +5 -8
  65. package/dist/learning-off/agents/triage.md +170 -0
  66. package/dist/learning-off/commands/bug-analysis.md +420 -0
  67. package/dist/learning-off/commands/code-review.md +530 -0
  68. package/dist/learning-off/commands/debug.md +314 -0
  69. package/dist/learning-off/commands/dynamic-build.md +1245 -0
  70. package/dist/learning-off/commands/dynamic-plan.md +426 -0
  71. package/dist/learning-off/commands/dynamic-profile.md +214 -0
  72. package/dist/learning-off/commands/dynamic-tickets.md +634 -0
  73. package/dist/learning-off/commands/explore.md +210 -0
  74. package/dist/learning-off/commands/implement.md +774 -0
  75. package/dist/learning-off/commands/plan.md +671 -0
  76. package/dist/learning-off/commands/release.md +311 -0
  77. package/dist/learning-off/commands/research.md +229 -0
  78. package/dist/learning-off/commands/resolve.md +842 -0
  79. package/dist/learning-off/commands/self-review.md +266 -0
  80. package/dist/skills/git/references/learn-conventions.md +27 -3
  81. package/dist/skills/git/references/pr/check-ci-status.md +19 -0
  82. package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
  83. package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
  84. package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
  85. package/dist/skills/git/references/pr/post-review-summary.md +20 -0
  86. package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
  87. package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
  88. package/dist/skills/git/references/pr/validate-branch.md +26 -0
  89. package/dist/skills/git/references/tracker/_contract.md +33 -0
  90. package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
  91. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
  92. package/dist/skills/git/references/tracker/github/create-release.md +30 -1
  93. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
  94. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
  95. package/dist/skills/git/references/tracker/github/fetch-issue.md +38 -1
  96. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +49 -1
  97. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +29 -0
  98. package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
  99. package/dist/skills/git/references/tracker/github/post-wave-report.md +17 -1
  100. package/dist/skills/git/references/tracker/github/setup-task.md +53 -1
  101. package/dist/skills/git/references/tracker/jira/associate-release.md +25 -1
  102. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
  103. package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
  104. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
  105. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
  106. package/dist/skills/git/references/tracker/jira/fetch-issue.md +38 -1
  107. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +49 -1
  108. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +29 -0
  109. package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
  110. package/dist/skills/git/references/tracker/jira/post-wave-report.md +17 -1
  111. package/dist/skills/git/references/tracker/jira/setup-task.md +55 -3
  112. package/dist/skills/git/references/tracker/linear/associate-release.md +25 -1
  113. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
  114. package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
  115. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
  116. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
  117. package/dist/skills/git/references/tracker/linear/fetch-issue.md +38 -1
  118. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +49 -1
  119. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +29 -0
  120. package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
  121. package/dist/skills/git/references/tracker/linear/post-wave-report.md +17 -1
  122. package/dist/skills/git/references/tracker/linear/setup-task.md +55 -3
  123. package/dist/targets/claude-code/agent-type-owner.js +164 -0
  124. package/dist/targets/claude-code/behavior-install.js +111 -0
  125. package/dist/targets/claude-code/installer.js +140 -37
  126. package/dist/targets/claude-code/language-stamp.js +185 -0
  127. package/dist/targets/claude-code/learning-install.js +497 -0
  128. package/dist/targets/claude-code/tracker-install.js +33 -16
  129. package/dist/targets/claude-code/tracker-servers.js +107 -0
  130. package/package.json +4 -3
  131. package/src/assets/agents/_partials/_code.mds +109 -0
  132. package/src/assets/agents/code-edit.mds +81 -0
  133. package/src/assets/agents/code-fix.mds +113 -0
  134. package/src/assets/agents/code.mds +135 -0
  135. package/src/assets/agents/design.mds +148 -0
  136. package/src/assets/agents/diagnose.mds +225 -0
  137. package/src/assets/agents/evaluate.md +8 -4
  138. package/src/assets/agents/explore.md +58 -0
  139. package/src/assets/agents/git.mds +33 -722
  140. package/src/assets/agents/knowledge.mds +131 -0
  141. package/src/assets/agents/learning.md +19 -69
  142. package/src/assets/agents/research.mds +182 -0
  143. package/src/assets/agents/review.mds +282 -0
  144. package/src/assets/agents/scrutinize.mds +135 -0
  145. package/src/assets/agents/simplify.md +3 -0
  146. package/src/assets/agents/skim.mds +165 -0
  147. package/src/assets/agents/synthesize.md +26 -352
  148. package/src/assets/agents/test.md +8 -0
  149. package/src/assets/agents/tracker.md +20 -7
  150. package/src/assets/agents/triage.mds +201 -0
  151. package/src/assets/agents/validate.md +13 -6
  152. package/src/assets/commands/_partials/_compliance.mds +41 -4
  153. package/src/assets/commands/_partials/_decisions.mds +33 -2
  154. package/src/assets/commands/_partials/_engine.mds +14 -6
  155. package/src/assets/commands/_partials/_knowledge.mds +25 -12
  156. package/src/assets/commands/_partials/_preamble.mds +34 -10
  157. package/src/assets/commands/_partials/_publication.mds +5 -4
  158. package/src/assets/commands/_partials/_roster.mds +4 -3
  159. package/src/assets/commands/_partials/_settings.mds +13 -5
  160. package/src/assets/commands/_partials/_wave.mds +9 -1
  161. package/src/assets/commands/bug-analysis.mds +27 -5
  162. package/src/assets/commands/code-review.mds +158 -52
  163. package/src/assets/commands/debug.mds +41 -3
  164. package/src/assets/commands/dynamic-build.mds +47 -27
  165. package/src/assets/commands/dynamic-plan.mds +40 -9
  166. package/src/assets/commands/dynamic-profile.mds +9 -1
  167. package/src/assets/commands/dynamic-tickets.mds +21 -6
  168. package/src/assets/commands/explore.mds +28 -2
  169. package/src/assets/commands/implement.mds +126 -129
  170. package/src/assets/commands/plan.mds +58 -14
  171. package/src/assets/commands/{release.md → release.mds} +30 -26
  172. package/src/assets/commands/research.mds +42 -11
  173. package/src/assets/commands/resolve.mds +59 -15
  174. package/src/assets/commands/self-review.mds +30 -5
  175. package/src/assets/mds/.mdsroot +4 -0
  176. package/src/assets/mds/git/_ops.mds +383 -0
  177. package/src/assets/mds/git/_ops_pr.mds +201 -0
  178. package/src/assets/mds/git/_ops_steps.mds +147 -0
  179. package/src/assets/mds/git/_pr.mds +39 -44
  180. package/src/assets/mds/git/_references.mds +6 -3
  181. package/src/assets/mds/tracker/_common.mds +5 -5
  182. package/src/assets/mds/tracker/_contract.mds +73 -0
  183. package/src/assets/mds/tracker/_github.mds +73 -16
  184. package/src/assets/mds/tracker/_jira.mds +77 -19
  185. package/src/assets/mds/tracker/_linear.mds +77 -19
  186. package/src/assets/mds/tracker/_mcp.mds +2 -2
  187. package/src/assets/mds/tracker/_steps.mds +98 -0
  188. package/src/assets/rules/behavior.md +6 -0
  189. package/src/assets/rules/context-economy.md +10 -0
  190. package/src/assets/rules/go.md +1 -0
  191. package/src/assets/rules/java.md +1 -0
  192. package/src/assets/rules/python.md +1 -0
  193. package/src/assets/rules/rust.md +1 -0
  194. package/src/assets/rules/typescript.md +1 -0
  195. package/src/assets/scripts/claude-md-audit.cjs +611 -0
  196. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
  197. package/src/assets/scripts/hooks/json-helper.cjs +13 -5
  198. package/src/assets/scripts/hooks/json-parse +34 -10
  199. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
  200. package/src/assets/scripts/hooks/session-start-context +315 -7
  201. package/src/assets/skills/agent-references/SKILL.md +33 -0
  202. package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
  203. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
  204. package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
  205. package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
  206. package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
  207. package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
  208. package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
  209. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  210. package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
  211. package/src/assets/skills/compliance/SKILL.md +1 -1
  212. package/src/assets/skills/docs-framework/SKILL.md +80 -83
  213. package/src/assets/skills/docs-framework/references/patterns.md +14 -25
  214. package/src/assets/skills/docs-framework/references/violations.md +7 -7
  215. package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
  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/quality-gates/SKILL.md +1 -1
  219. package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
  220. package/src/assets/agents/code.md +0 -330
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Language-focus stamp for the installed /code-review command.
3
+ *
4
+ * D-LANGUAGE-FOCUS-STAMP: /code-review spawns a language focus only when its file-type trigger fires AND
5
+ * the focus appears on one fixed-prefix line of the installed command. That line replaces the run-time
6
+ * `test -f` probe of Claude Code's directory; the installer writes it from the registry and the effective
7
+ * selection ({@link installedLanguageFocuses}), so the prompt reads a fact instead of discovering one.
8
+ * Decided here:
9
+ *
10
+ * - The carrier is one line, `Installed language focuses: <list>`, shipped as `(none)` in
11
+ * src/assets/commands/code-review.mds and found again by an anchored match, so a later run can
12
+ * re-stamp a copy an earlier run stamped. It carries no MDS interpolation braces, and it sits outside
13
+ * every learning arm, so both variants carry it. A separate stamped file would add a file to the
14
+ * install walk and the install goldens, which an in-place rewrite leaves unchanged.
15
+ * - Only code-review.md carries it. /dynamic-build spawns every review focus and drops none (plan
16
+ * decision 1), so it has no stamp, no reduced classes and no DIFF_FILE.
17
+ * - The stamp describes what is on disk, so it is computed from the EFFECTIVE selection
18
+ * (`FileCopyOptions.effectivePlugins`), never from the plugins one run copies, and it is applied after
19
+ * the command copy and the orphan sweep, whether or not code-review's own plugin is in the run. The
20
+ * rewrite compares the bytes it is about to write with the bytes now on disk, at the moment it writes,
21
+ * so no earlier same-run copy or pre-clean can defeat it.
22
+ * - `devflow uninstall --plugin` removes a language plugin's skill but no command, so its name would
23
+ * stay in the stamped copy; the selective phase re-stamps from the plugins that remain, through
24
+ * {@link restampInstalledCommands} like the install.
25
+ * - Composition with the learning converge (D-LEARNING-VARIANT-INSTALL), pinned: the converge rewrites
26
+ * installed command files from dist or dist/learning-off, and those files carry the shipped `(none)`
27
+ * line. It therefore applies {@link carryLanguageStamp} inside its own write path, putting the stamp
28
+ * it finds on the installed copy into the variant it writes ({@link stampForConverge}); a copy with no
29
+ * list to carry, one installed before the stamp existed, is stamped from the converge's selection
30
+ * instead of being given the shipped `(none)`. The alternative, re-stamping after every
31
+ * converge write, would make a steady-state run see a stamped copy that differs from its source, rewrite
32
+ * it and re-stamp it on every `devflow init`; carrying keeps the converge's byte comparison honest, so a
33
+ * run that changes nothing writes nothing, and it leaves one authority for the list: install and
34
+ * uninstall change it, a variant switch never does.
35
+ * - No prompt rule for a damaged stamp exists: no legitimate configuration produces one. A copy with
36
+ * no stamp line, or several, is reported here as a warning and left as it is.
37
+ * - Nothing here decides a diff's class or which focuses a diff gets. Those are prompt rules the
38
+ * orchestrator executes; this module only turns the registry and a selection into a list.
39
+ *
40
+ * Never throws: every failure is a warning, because the install must not die on a stamp.
41
+ */
42
+ import { promises as fs } from 'fs';
43
+ import * as path from 'path';
44
+ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
45
+ import { mdFileName } from '../../core/orphan-sweep.js';
46
+ import { installedLanguageFocuses } from '../../core/plugins.js';
47
+ /** The fixed prefix of the stamp line; the prompt and the anchored match share it. */
48
+ export const LANGUAGE_STAMP_PREFIX = 'Installed language focuses: ';
49
+ /** What an empty list renders as, and what the build ships. */
50
+ export const LANGUAGE_STAMP_NONE = '(none)';
51
+ /** Installed commands that carry the stamp line. Only /code-review (plan decision 1). */
52
+ export const LANGUAGE_STAMPED_COMMANDS = ['code-review'];
53
+ /** The most focuses one stamp lists; the registry has eight, so the bound is generous. */
54
+ const MAX_STAMPED_FOCUSES = 16;
55
+ /** One focus name is a plain lowercase skill name. */
56
+ const FOCUS_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
57
+ /** A well-formed list as rendered: `(none)`, or comma-space separated plain names. */
58
+ const STAMP_VALUE_RE = /^(?:\(none\)|[a-z][a-z0-9-]{0,31}(?:, [a-z][a-z0-9-]{0,31}){0,15})$/;
59
+ /** The stamp line, anchored to a whole line. Rebuilt per use: a global regex carries state. */
60
+ const stampLineRe = () => /^Installed language focuses: [^\r\n]*$/gm;
61
+ /** The full stamp line for a list. */
62
+ export function renderLanguageStamp(focuses) {
63
+ return `${LANGUAGE_STAMP_PREFIX}${focuses.length === 0 ? LANGUAGE_STAMP_NONE : focuses.join(', ')}`;
64
+ }
65
+ /**
66
+ * `content` with its one stamp line replaced by the rendering of `focuses`.
67
+ *
68
+ * Pure. Refuses a list holding anything but plain skill names, so no prompt text can ride in on it, and
69
+ * refuses a file with no stamp line or several rather than guessing which one is the carrier.
70
+ */
71
+ export function applyLanguageStamp(content, focuses) {
72
+ if (focuses.length > MAX_STAMPED_FOCUSES || !focuses.every(name => FOCUS_NAME_RE.test(name))) {
73
+ return { ok: false, reason: 'bad-focus-name' };
74
+ }
75
+ const lines = [...content.matchAll(stampLineRe())];
76
+ if (lines.length === 0)
77
+ return { ok: false, reason: 'no-stamp-line' };
78
+ if (lines.length > 1)
79
+ return { ok: false, reason: 'several-stamp-lines' };
80
+ const at = lines[0].index ?? 0;
81
+ return { ok: true, content: content.slice(0, at) + renderLanguageStamp(focuses) + content.slice(at + lines[0][0].length) };
82
+ }
83
+ /**
84
+ * `source` with its stamp line replaced by the one on `installed`.
85
+ *
86
+ * The learning converge's write path (see the header): the variant it installs keeps the list the
87
+ * install stamped. Returns `source` unchanged unless both sides hold exactly one stamp line and the
88
+ * installed one is a well-formed list, so a damaged or hostile line is never copied into a fresh file.
89
+ */
90
+ export function carryLanguageStamp(source, installed) {
91
+ const carried = carriableStampLine(installed);
92
+ const into = [...source.matchAll(stampLineRe())];
93
+ if (carried === null || into.length !== 1)
94
+ return source;
95
+ const at = into[0].index ?? 0;
96
+ return source.slice(0, at) + carried + source.slice(at + into[0][0].length);
97
+ }
98
+ /** The installed copy's one stamp line when it holds exactly one and its list is well formed, else null. */
99
+ function carriableStampLine(installed) {
100
+ const from = [...installed.matchAll(stampLineRe())];
101
+ if (from.length !== 1)
102
+ return null;
103
+ return STAMP_VALUE_RE.test(from[0][0].slice(LANGUAGE_STAMP_PREFIX.length)) ? from[0][0] : null;
104
+ }
105
+ /**
106
+ * The text the learning converge installs for a stamped command: `source` carrying the installed copy's
107
+ * list ({@link carryLanguageStamp}) when the copy holds one, else `source` stamped with `focuses`, the
108
+ * effective selection's list, exactly as the install stamps it.
109
+ *
110
+ * A copy with no list to carry was installed by a version before the stamp existed, or is damaged. A
111
+ * `--plugin` init right after an upgrade reaches such a copy through the converge alone (the run copies
112
+ * no command), so installing the shipped `(none)` over it would drop every language focus until the next
113
+ * full install. A list that IS carried is never changed here. Pure.
114
+ */
115
+ export function stampForConverge(source, installed, focuses) {
116
+ if (carriableStampLine(installed) !== null)
117
+ return carryLanguageStamp(source, installed);
118
+ const stamped = applyLanguageStamp(source, focuses);
119
+ return stamped.ok ? stamped.content : source;
120
+ }
121
+ /**
122
+ * Stamp every installed copy that carries the stamp with the list for `effectivePlugins`.
123
+ *
124
+ * Called by installViaFileCopy after the command copy and the orphan sweep (every install shape) and by
125
+ * the selective uninstall phase after the removals. A command that is not installed is left absent, so a
126
+ * `--plugin` run never gains a command it did not install.
127
+ */
128
+ export async function restampInstalledCommands(opts) {
129
+ const { claudeDir, effectivePlugins, warn, mayChange } = opts;
130
+ const focuses = installedLanguageFocuses(effectivePlugins);
131
+ const rewritten = [];
132
+ const unchanged = [];
133
+ const absent = [];
134
+ const failed = [];
135
+ // Precondition, asserted in production code: a relative claudeDir would resolve the targets
136
+ // against the working directory.
137
+ if (!path.isAbsolute(claudeDir)) {
138
+ warn(`language stamp: claudeDir is not an absolute path ("${claudeDir}") — the installed commands were not stamped`);
139
+ return { focuses, rewritten, unchanged, absent, failed: [...LANGUAGE_STAMPED_COMMANDS] };
140
+ }
141
+ for (const command of LANGUAGE_STAMPED_COMMANDS) {
142
+ const target = path.join(claudeDir, 'commands', 'devflow', mdFileName(command));
143
+ let current;
144
+ try {
145
+ current = await fs.readFile(target, 'utf-8');
146
+ }
147
+ catch (err) {
148
+ if (err.code === 'ENOENT') {
149
+ absent.push(command);
150
+ }
151
+ else {
152
+ warn(`language stamp: cannot read ${mdFileName(command)} (${target}) — ${String(err)}`);
153
+ failed.push(command);
154
+ }
155
+ continue;
156
+ }
157
+ const stamped = applyLanguageStamp(current, focuses);
158
+ if (!stamped.ok) {
159
+ warn(stamped.reason === 'no-stamp-line'
160
+ ? `language stamp: ${mdFileName(command)} has no stamp line, so the installed language focuses were not recorded. Run \`devflow init\` to reinstall it.`
161
+ : `language stamp: ${mdFileName(command)} has ${stamped.reason === 'several-stamp-lines' ? 'several stamp lines' : 'an unusable language list'}, so it was left as it is. Run \`devflow init\` to reinstall it.`);
162
+ failed.push(command);
163
+ continue;
164
+ }
165
+ // Compared at the moment of writing, against what is on disk now.
166
+ if (stamped.content === current) {
167
+ unchanged.push(command);
168
+ continue;
169
+ }
170
+ try {
171
+ if (mayChange !== undefined && !(await mayChange(target))) {
172
+ failed.push(command);
173
+ continue;
174
+ }
175
+ await writeFileAtomicExclusive(target, stamped.content);
176
+ rewritten.push(command);
177
+ }
178
+ catch (err) {
179
+ warn(`language stamp: could not rewrite ${mdFileName(command)} (${target}) — ${String(err)}`);
180
+ failed.push(command);
181
+ }
182
+ }
183
+ return { focuses, rewritten, unchanged, absent, failed };
184
+ }
185
+ //# sourceMappingURL=language-stamp.js.map
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Learning-variant installer for the Claude Code target.
3
+ *
4
+ * D-LEARNING-VARIANT-INSTALL: the install follows the machine's learning switch
5
+ * (`features.learning` in ~/.devflow/manifest.json) and nothing else. When the
6
+ * switch is off, every command and agent that has a learning-off variant
7
+ * (dist/learning-off/{commands,agents}, built by D-LEARNING-VARIANTS) is installed
8
+ * from it, and the apply-decisions skill is not installed, so the decisions text
9
+ * reaches no prompt and the skill has no reader. When the switch is on, the
10
+ * learning-on files and the skill are installed. Three pieces carry it:
11
+ *
12
+ * - the source order (src/core/assets.ts commandSourceDirs / agentSourceDirs):
13
+ * with learning off the learning-off directory is consulted first, then the
14
+ * normal order, so a host with no arm installs its one file as before;
15
+ * - the skill condition (LEARNING_GATED_SKILLS in src/core/plugins.ts):
16
+ * installViaFileCopy leaves the skill out when learning is off;
17
+ * - {@link convergeLearningVariants}, below: the converge that makes an
18
+ * ALREADY-installed tree match the switch, run by `devflow init` after the
19
+ * file copy and by `devflow learning --enable/--disable` after the switch is
20
+ * written ({@link applyLearningToggle}).
21
+ *
22
+ * The converge, decided in turn:
23
+ * - Its roster is the files that exist under dist/learning-off, not a list typed
24
+ * here: the build owns which hosts have an arm, so a new arm is converged with
25
+ * no edit to this file.
26
+ * - It rewrites only files that are ALREADY installed. A `--plugin` install
27
+ * carries one plugin's files; a toggle must not add another plugin's. For the
28
+ * same reason the skill is installed on a switch-on only when the machine's
29
+ * plugin selection requires it, and removed on a switch-off only if it is
30
+ * there.
31
+ * - A write is atomic (temp file and rename) and byte-compared first: a file
32
+ * already holding the target bytes is never touched, so a steady-state run
33
+ * moves nothing and its counts say so (the unchanged-count the pre-clean
34
+ * lesson asks for).
35
+ * - Each file and the skill are tried separately, with per-item isolation: one
36
+ * failure never skips the rest of the fan-out. Every failure is a warning,
37
+ * never a throw. A converge failure only warns, because either mismatch is
38
+ * safe: an on variant is still gated at run time (`decisions_gate()`), and an
39
+ * off variant loads no decisions at all.
40
+ * - A shadow is honoured exactly as the install honours it (a valid shadow wins, an
41
+ * invalid one is reported and the shipped source installs), because
42
+ * the skill is resolved through the installer's own resolveSkillSource. Only
43
+ * skills and rules have shadows; commands and agents have none, so there is
44
+ * nothing for an off variant to collide with. A shadow of apply-decisions
45
+ * under learning off is DORMANT in the same sense as one for a deselected
46
+ * skill: kept in ~/.devflow/skills, never installed, never deleted, and
47
+ * applied again the moment learning comes back on.
48
+ * - Version skew: the variants in this package were built with this version.
49
+ * When the manifest records another version, the installed files are not the
50
+ * ones this package would rewrite, so {@link applyLearningToggle} skips the
51
+ * converge and says to run `devflow init`.
52
+ * - An agent's installed `model:` and `effort:` are carried into the variant before
53
+ * the byte comparison (D-AGENT-OVERRIDE-CARRY, see {@link withInstalledState}),
54
+ * so the overrides `devflow agents` and the proxy put there are never written
55
+ * back to the shipped values and a no-op converge writes no agent.
56
+ * {@link applyLearningToggle} still runs reapplyAgentMapping straight after a
57
+ * converge that rewrote an agent: it reads agent-models.json and is the authority.
58
+ *
59
+ * D-LEARNING-PRELOAD-MACHINE-ONLY (amended): the apply-decisions preload follows
60
+ * the machine switch only, never a repository's narrowing, because a repository
61
+ * can narrow learning off on a machine where it is on and the installed agents are
62
+ * machine-wide. Met by the variant: the learning-off agent files do not preload
63
+ * the skill (no install-time frontmatter rewrite exists), and the learning-on files
64
+ * keep the preload and their run-time gate for a narrowed repository. A repo
65
+ * `project.json` with `learning: false` on a learning-on machine therefore changes
66
+ * nothing installed.
67
+ *
68
+ * I/O orchestration in src/targets/; the pure selection rules stay in src/core/.
69
+ */
70
+ import { promises as fs } from 'fs';
71
+ import * as path from 'path';
72
+ import { agentSourceDirs, commandSourceDirs, learningOffDir, } from '../../core/assets.js';
73
+ import { carryStampedTools } from '../../core/agent-frontmatter.js';
74
+ import { carryAgentOverrides, reapplyAgentMapping } from '../../core/agent-models.js';
75
+ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
76
+ import { readManifest } from '../../core/manifest.js';
77
+ import { getPackageRoot } from '../../core/paths.js';
78
+ import { isProxyEnabled } from '../../core/proxy-state.js';
79
+ import { DEVFLOW_PLUGINS, LEARNING_GATED_SKILLS, installedLanguageFocuses, prefixSkillName, skillsOf, } from '../../core/plugins.js';
80
+ import { copyDirectory, resolveSkillSource } from './installer.js';
81
+ import { LANGUAGE_STAMPED_COMMANDS, stampForConverge } from './language-stamp.js';
82
+ // ── Bounds ─────────────────────────────────────────────────────────────────
83
+ /**
84
+ * The most variant files one kind may carry. The shipped roster is 14 commands and
85
+ * 9 agents; the bound exists so a directory that grew without limit cannot turn the
86
+ * converge into an unbounded loop (every loop has an explicit bound).
87
+ */
88
+ export const MAX_LEARNING_ROSTER = 256;
89
+ /** Deepest skill-directory descent the tree comparison takes. */
90
+ const MAX_SKILL_TREE_DEPTH = 4;
91
+ /** A roster entry is a flat, lowercase, hyphenated markdown basename. */
92
+ const ROSTER_NAME_RE = /^[a-z0-9][a-z0-9-]*\.md$/;
93
+ const KINDS = ['commands', 'agents'];
94
+ // ── Internals ──────────────────────────────────────────────────────────────
95
+ function installedFile(claudeDir, kind, fileName) {
96
+ return path.join(claudeDir, kind, 'devflow', fileName);
97
+ }
98
+ /** The learning-ON source directories for a kind, most-preferred first. */
99
+ function onSourceDirs(kind, root) {
100
+ return kind === 'commands' ? commandSourceDirs(true, root) : agentSourceDirs(root, true);
101
+ }
102
+ /**
103
+ * The roster of one kind: the `.md` files under dist/learning-off/<kind>, sorted.
104
+ *
105
+ * `present` is false when the directory does not exist (the build has not run, or
106
+ * only `build:cli` did), so the caller can say so instead of reporting a converge
107
+ * that had nothing to do.
108
+ */
109
+ async function readRoster(kind, root, warn) {
110
+ let entries;
111
+ try {
112
+ entries = await fs.readdir(learningOffDir(kind, root));
113
+ }
114
+ catch (err) {
115
+ if (err.code === 'ENOENT')
116
+ return { names: [], present: false };
117
+ warn(`learning variants: cannot read the ${kind} variants — ${String(err)}`);
118
+ return { names: [], present: false };
119
+ }
120
+ const names = entries.filter(entry => ROSTER_NAME_RE.test(entry)).sort();
121
+ if (names.length > MAX_LEARNING_ROSTER) {
122
+ warn(`learning variants: ${names.length} ${kind} variants exceed the bound of ${MAX_LEARNING_ROSTER}; converging the first ${MAX_LEARNING_ROSTER}`);
123
+ return { names: names.slice(0, MAX_LEARNING_ROSTER), present: true };
124
+ }
125
+ return { names, present: true };
126
+ }
127
+ async function readIfPresent(file) {
128
+ try {
129
+ return await fs.readFile(file);
130
+ }
131
+ catch (err) {
132
+ return err.code === 'ENOENT' ? 'absent' : 'unreadable';
133
+ }
134
+ }
135
+ /** First of `candidates` that exists, or undefined. Bounded by the candidate list. */
136
+ async function firstReadable(candidates) {
137
+ for (const file of candidates) {
138
+ try {
139
+ return { file, bytes: await fs.readFile(file) };
140
+ }
141
+ catch { /* not here — next directory in preference order */ }
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * The bytes a write would install: the variant `source` wearing the state the
147
+ * installed copy carries, so the byte comparison in {@link convergeFile} compares
148
+ * variants and nothing else.
149
+ *
150
+ * D-LANGUAGE-FOCUS-STAMP, composition: the variant on disk carries the language list the
151
+ * install stamped (code-review.md), and the source carries the shipped `(none)`. The stamp is
152
+ * carried from the installed copy into the text this write would install, so the byte
153
+ * comparison stays honest (a stamped copy of the right variant is `unchanged`, not
154
+ * rewritten on every run) and a variant switch never loses or changes the list. A copy with
155
+ * no list to carry (installed before the stamp existed) is stamped from the selection instead.
156
+ *
157
+ * D-AGENT-OVERRIDE-CARRY, composition: an installed agent carries the `model:` and `effort:`
158
+ * that `devflow agents` and the proxy put there, and the source carries the shipped ones.
159
+ * Carrying them keeps the override window shut (no write ever leaves an agent on its shipped
160
+ * model while reapplyAgentMapping has yet to restore it) and keeps a no-op converge
161
+ * write-free, so "N agent(s) rewritten" counts only real variant switches. reapplyAgentMapping
162
+ * stays the authority: it reads agent-models.json after the converge and the carry only agrees
163
+ * with it (see carryAgentOverrides).
164
+ *
165
+ * D-TRACKER-SERVER-ALLOWLIST, composition: the installed Git agent carries the tracker-server
166
+ * entries the install stamped onto its `tools:` line, and the variant source carries none.
167
+ * The stamp is carried the same way (carryStampedTools), so a variant switch keeps the list
168
+ * and a copy already holding the right variant and its stamp compares byte-equal. Install
169
+ * and `devflow tracker --servers` are the only writers of the list; a converge never changes it.
170
+ *
171
+ * Pure.
172
+ */
173
+ function withInstalledState(kind, fileName, source, installed, focuses) {
174
+ if (kind === 'agents') {
175
+ const sourceText = source.toString('utf-8');
176
+ const installedText = installed.toString('utf-8');
177
+ const carried = carryStampedTools(carryAgentOverrides(sourceText, installedText), installedText);
178
+ return carried === sourceText ? source : Buffer.from(carried, 'utf-8');
179
+ }
180
+ return LANGUAGE_STAMPED_COMMANDS.includes(fileName.replace(/\.md$/, ''))
181
+ ? Buffer.from(stampForConverge(source.toString('utf-8'), installed.toString('utf-8'), focuses), 'utf-8')
182
+ : source;
183
+ }
184
+ /**
185
+ * Converge ONE installed prompt onto the variant the switch wants.
186
+ *
187
+ * Never throws: every failure is a warning and the outcome `failed`, so the next
188
+ * file is still tried.
189
+ */
190
+ async function convergeFile(kind, fileName, opts) {
191
+ const { claudeDir, learning, warn, root, focuses } = opts;
192
+ const target = installedFile(claudeDir, kind, fileName);
193
+ const installed = await readIfPresent(target);
194
+ if (installed === 'absent')
195
+ return 'not-installed';
196
+ if (installed === 'unreadable') {
197
+ warn(`learning variants: cannot read the installed ${kind}/${fileName} (${target}) — left as it is`);
198
+ return 'failed';
199
+ }
200
+ // Learning off: the variant is the roster file itself. Learning on: the normal
201
+ // source order, which is where the learning-on file of that host lives.
202
+ const candidates = learning
203
+ ? onSourceDirs(kind, root).map(dir => path.join(dir, fileName))
204
+ : [path.join(learningOffDir(kind, root), fileName)];
205
+ const source = await firstReadable(candidates);
206
+ if (source === undefined) {
207
+ warn(`learning variants: no source for ${kind}/${fileName} (searched: ${candidates.join(', ')}) — left as it is`);
208
+ return 'failed';
209
+ }
210
+ const wanted = withInstalledState(kind, fileName, source.bytes, installed, focuses);
211
+ // Byte-compared: an installed file already holding the target bytes is not
212
+ // rewritten, so a run that changes nothing writes nothing.
213
+ if (installed.equals(wanted))
214
+ return 'unchanged';
215
+ try {
216
+ // Prompts are UTF-8 text; the atomic writer takes a string and preserves the
217
+ // target's permission mode.
218
+ await writeFileAtomicExclusive(target, wanted.toString('utf-8'));
219
+ }
220
+ catch (err) {
221
+ warn(`learning variants: could not rewrite ${kind}/${fileName} (${target}) — ${String(err)}`);
222
+ return 'failed';
223
+ }
224
+ return 'rewritten';
225
+ }
226
+ /** Every file below `dir` as sorted relative paths; null when anything is not a plain file or directory. */
227
+ async function listTree(dir, depth = 0) {
228
+ if (depth > MAX_SKILL_TREE_DEPTH)
229
+ return null;
230
+ let entries;
231
+ try {
232
+ entries = await fs.readdir(dir, { withFileTypes: true });
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ const out = [];
238
+ for (const entry of entries) {
239
+ if (entry.isDirectory()) {
240
+ const nested = await listTree(path.join(dir, entry.name), depth + 1);
241
+ if (nested === null)
242
+ return null;
243
+ out.push(...nested.map(rel => path.join(entry.name, rel)));
244
+ }
245
+ else if (entry.isFile()) {
246
+ out.push(entry.name);
247
+ }
248
+ else {
249
+ return null;
250
+ }
251
+ }
252
+ return out.sort();
253
+ }
254
+ /** True only when both trees hold the same files with the same bytes. Any error answers false. */
255
+ async function treesIdentical(a, b) {
256
+ const [left, right] = await Promise.all([listTree(a), listTree(b)]);
257
+ if (left === null || right === null || left.length !== right.length)
258
+ return false;
259
+ for (let i = 0; i < left.length; i++) {
260
+ if (left[i] !== right[i])
261
+ return false;
262
+ try {
263
+ const [x, y] = await Promise.all([fs.readFile(path.join(a, left[i])), fs.readFile(path.join(b, right[i]))]);
264
+ if (!x.equals(y))
265
+ return false;
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ }
271
+ return true;
272
+ }
273
+ async function pathExists(p) {
274
+ try {
275
+ await fs.access(p);
276
+ return true;
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
282
+ /**
283
+ * Install one skill directory atomically: stage a copy beside the claude dir's
284
+ * skills, then swap it in, restoring the old directory if the swap fails.
285
+ *
286
+ * Staged under `claudeDir` itself, not under `skills/`: a staging directory a
287
+ * crash leaves behind must not look like a second skill to Claude Code.
288
+ */
289
+ async function swapInSkillDirectory(claudeDir, sourceDir, target) {
290
+ const staging = path.join(claudeDir, `.devflow-learning-${process.pid}-${Date.now().toString(36)}.tmp`);
291
+ const backup = `${staging}.old`;
292
+ try {
293
+ await copyDirectory(sourceDir, staging);
294
+ await fs.mkdir(path.dirname(target), { recursive: true });
295
+ const hadTarget = await pathExists(target);
296
+ if (hadTarget)
297
+ await fs.rename(target, backup);
298
+ try {
299
+ await fs.rename(staging, target);
300
+ }
301
+ catch (err) {
302
+ if (hadTarget)
303
+ await fs.rename(backup, target);
304
+ throw err;
305
+ }
306
+ }
307
+ finally {
308
+ await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
309
+ await fs.rm(backup, { recursive: true, force: true }).catch(() => undefined);
310
+ }
311
+ }
312
+ /** Converge the learning-gated skill directories. Never throws. */
313
+ async function convergeSkills(opts) {
314
+ const { claudeDir, devflowDir, learning, plugins, warn } = opts;
315
+ const selected = skillsOf(plugins);
316
+ let state = 'unchanged';
317
+ // Every gated skill is converged: a failure on one must not skip the next.
318
+ for (const skill of LEARNING_GATED_SKILLS) {
319
+ const target = path.join(claudeDir, 'skills', prefixSkillName(skill));
320
+ try {
321
+ if (!learning) {
322
+ if (!(await pathExists(target)))
323
+ continue;
324
+ await fs.rm(target, { recursive: true });
325
+ state = 'removed';
326
+ continue;
327
+ }
328
+ if (!selected.has(skill)) {
329
+ if (state === 'unchanged')
330
+ state = 'not-selected';
331
+ continue;
332
+ }
333
+ const resolved = await resolveSkillSource(skill, devflowDir, opts.packageRoot);
334
+ if ((await pathExists(target)) && (await treesIdentical(resolved.dir, target)))
335
+ continue;
336
+ await swapInSkillDirectory(claudeDir, resolved.dir, target);
337
+ state = 'installed';
338
+ }
339
+ catch (err) {
340
+ warn(`learning variants: could not converge the ${prefixSkillName(skill)} skill (${target}) — ${String(err)}`);
341
+ state = 'failed';
342
+ }
343
+ }
344
+ return state;
345
+ }
346
+ // ── Convergence ────────────────────────────────────────────────────────────
347
+ /**
348
+ * Make the installed commands, agents and apply-decisions skill match the
349
+ * machine's learning switch.
350
+ *
351
+ * Never throws. A caller reads `converged`; it does not catch. A claudeDir that is
352
+ * not absolute is reported the same way rather than thrown, because an install
353
+ * must not die on it.
354
+ */
355
+ export async function convergeLearningVariants(opts) {
356
+ const { claudeDir, learning, warn } = opts;
357
+ const empty = {
358
+ converged: false, commandsRewritten: [], agentsRewritten: [], unchanged: 0, notInstalled: 0, skill: 'unchanged',
359
+ };
360
+ // Precondition, asserted in production code: a relative claudeDir would resolve
361
+ // the install targets somewhere unexpected.
362
+ if (!path.isAbsolute(claudeDir)) {
363
+ warn(`learning variants: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
364
+ return empty;
365
+ }
366
+ const root = opts.packageRoot ?? getPackageRoot();
367
+ const focuses = installedLanguageFocuses(opts.plugins);
368
+ let failed = false;
369
+ let unchanged = 0;
370
+ let notInstalled = 0;
371
+ const rewritten = { commands: [], agents: [] };
372
+ for (const kind of KINDS) {
373
+ const roster = await readRoster(kind, root, warn);
374
+ if (!roster.present) {
375
+ warn(`learning variants: no ${kind} variants under ${learningOffDir(kind, root)} — run \`npm run build:mds\`; installed ${kind} left as they are`);
376
+ failed = true;
377
+ continue;
378
+ }
379
+ for (const fileName of roster.names) {
380
+ const outcome = await convergeFile(kind, fileName, { claudeDir, learning, warn, root, focuses });
381
+ if (outcome === 'rewritten')
382
+ rewritten[kind].push(fileName);
383
+ else if (outcome === 'unchanged')
384
+ unchanged++;
385
+ else if (outcome === 'not-installed')
386
+ notInstalled++;
387
+ else
388
+ failed = true;
389
+ }
390
+ }
391
+ // Evaluated unconditionally, after the files: a failed prompt never skips the skill
392
+ // (one failure must not leave the rest of the fan-out half-converged).
393
+ const skill = await convergeSkills(opts);
394
+ if (skill === 'failed')
395
+ failed = true;
396
+ return {
397
+ converged: !failed,
398
+ commandsRewritten: rewritten.commands,
399
+ agentsRewritten: rewritten.agents,
400
+ unchanged,
401
+ notInstalled,
402
+ skill,
403
+ };
404
+ }
405
+ // ── The toggle ─────────────────────────────────────────────────────────────
406
+ /** The running package's version, or null when its package.json cannot be read or holds none. */
407
+ export async function readRunningVersion(packageRoot = getPackageRoot()) {
408
+ try {
409
+ const pkg = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf-8'));
410
+ return typeof pkg.version === 'string' && pkg.version !== '' ? pkg.version : null;
411
+ }
412
+ catch {
413
+ return null;
414
+ }
415
+ }
416
+ /**
417
+ * One line for what a converge changed, or null when it changed nothing.
418
+ *
419
+ * The caller prints it only on a toggle: a run that moved nothing says nothing.
420
+ */
421
+ export function describeLearningConverge(result, learning) {
422
+ const parts = [];
423
+ const prompts = result.commandsRewritten.length + result.agentsRewritten.length;
424
+ if (prompts > 0) {
425
+ parts.push(`${result.commandsRewritten.length} command(s) and ${result.agentsRewritten.length} agent(s) ` +
426
+ `rewritten for the learning-${learning ? 'on' : 'off'} variant`);
427
+ }
428
+ if (result.skill === 'installed')
429
+ parts.push('the apply-decisions skill installed');
430
+ if (result.skill === 'removed')
431
+ parts.push('the apply-decisions skill removed');
432
+ return parts.length === 0 ? null : parts.join('; ');
433
+ }
434
+ /**
435
+ * The version-skew check, apart from the I/O so it is provable by itself: null when
436
+ * the installed prompts and this package agree, else the message to print.
437
+ */
438
+ export function learningToggleSkew(installedVersion, runningVersion, learning) {
439
+ if (runningVersion !== null && installedVersion === runningVersion)
440
+ return null;
441
+ return (`The installed prompts come from devflow ${installedVersion} and this CLI is ${runningVersion ?? 'of unknown version'}, ` +
442
+ `so the learning-${learning ? 'on' : 'off'} variants were not applied. Run \`devflow init\` to apply them.`);
443
+ }
444
+ /**
445
+ * What `devflow learning --enable/--disable` does after the switch is written:
446
+ * converge the installed tree onto the new value, then reapply the saved agent
447
+ * model and effort mapping.
448
+ *
449
+ * The converge carries each installed agent's model and effort into the variant it
450
+ * writes (D-AGENT-OVERRIDE-CARRY), so the overrides are already on the file and this
451
+ * reapply is the authority behind that carry, not a repair of a default window: it
452
+ * re-reads agent-models.json and corrects any agent whose installed values had
453
+ * drifted from it. It runs only when the converge rewrote an agent, and a failure
454
+ * there is a warning like every other failure here.
455
+ *
456
+ * Never throws.
457
+ */
458
+ export async function applyLearningToggle(opts) {
459
+ const { claudeDir, devflowDir, learning, runningVersion, warn } = opts;
460
+ const manifest = await readManifest(devflowDir);
461
+ if (manifest === null) {
462
+ return {
463
+ kind: 'skipped',
464
+ reason: 'no-manifest',
465
+ message: 'No readable install manifest, so the learning variants were not applied. Run `devflow init`.',
466
+ };
467
+ }
468
+ const skew = learningToggleSkew(manifest.version, runningVersion, learning);
469
+ if (skew !== null)
470
+ return { kind: 'skipped', reason: 'version-skew', message: skew };
471
+ const selection = new Set(manifest.plugins);
472
+ const result = await convergeLearningVariants({
473
+ claudeDir,
474
+ devflowDir,
475
+ learning,
476
+ plugins: DEVFLOW_PLUGINS.filter(plugin => selection.has(plugin.name)),
477
+ warn,
478
+ packageRoot: opts.packageRoot,
479
+ });
480
+ let agentMappingReapplied = false;
481
+ if (result.agentsRewritten.length > 0) {
482
+ try {
483
+ await reapplyAgentMapping({
484
+ proxyEnabled: await isProxyEnabled(devflowDir),
485
+ installDir: path.join(claudeDir, 'agents', 'devflow'),
486
+ devflowDir,
487
+ onWarning: warn,
488
+ });
489
+ agentMappingReapplied = true;
490
+ }
491
+ catch (err) {
492
+ warn(`learning variants: could not reapply the agent model mapping — ${String(err)}`);
493
+ }
494
+ }
495
+ return { kind: 'converged', result, agentMappingReapplied };
496
+ }
497
+ //# sourceMappingURL=learning-install.js.map