devflow-kit 3.3.0 → 3.4.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 (138) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/agents/code.md +330 -0
  3. package/{src/assets → dist}/agents/design.md +1 -1
  4. package/{src/assets → dist}/agents/diagnose.md +1 -2
  5. package/dist/agents/git.md +29 -56
  6. package/{src/assets → dist}/agents/knowledge.md +4 -3
  7. package/{src/assets → dist}/agents/research.md +2 -2
  8. package/{src/assets → dist}/agents/review.md +8 -7
  9. package/{src/assets → dist}/agents/scrutinize.md +1 -1
  10. package/dist/agents/skim.md +148 -0
  11. package/{src/assets → dist}/agents/triage.md +1 -1
  12. package/dist/cli/commands/init.js +62 -0
  13. package/dist/cli/commands/learning.js +38 -3
  14. package/dist/cli/commands/uninstall.js +42 -1
  15. package/dist/commands/bug-analysis.md +30 -8
  16. package/dist/commands/code-review.md +141 -60
  17. package/dist/commands/debug.md +14 -12
  18. package/dist/commands/dynamic-build.md +37 -38
  19. package/dist/commands/dynamic-plan.md +30 -18
  20. package/dist/commands/dynamic-profile.md +27 -13
  21. package/dist/commands/dynamic-tickets.md +28 -14
  22. package/dist/commands/explore.md +15 -13
  23. package/dist/commands/implement.md +33 -28
  24. package/dist/commands/plan.md +37 -24
  25. package/dist/commands/release.md +69 -4
  26. package/dist/commands/research.md +33 -11
  27. package/dist/commands/resolve.md +35 -32
  28. package/dist/commands/self-review.md +36 -23
  29. package/dist/core/agent-models.js +43 -0
  30. package/dist/core/assets.js +55 -10
  31. package/dist/core/claude-md-audit.js +190 -0
  32. package/dist/core/feature-switch.js +20 -1
  33. package/dist/core/flags.js +28 -0
  34. package/dist/core/fs-atomic.js +8 -3
  35. package/dist/core/learning-variants.js +213 -0
  36. package/dist/core/manifest.js +62 -0
  37. package/dist/core/mds-variants.js +38 -1
  38. package/dist/core/plugins.js +71 -9
  39. package/{src/assets → dist/learning-off}/agents/code.md +6 -10
  40. package/dist/learning-off/agents/design.md +119 -0
  41. package/dist/learning-off/agents/diagnose.md +210 -0
  42. package/dist/learning-off/agents/knowledge.md +90 -0
  43. package/dist/learning-off/agents/research.md +149 -0
  44. package/dist/learning-off/agents/review.md +228 -0
  45. package/dist/learning-off/agents/scrutinize.md +117 -0
  46. package/{src/assets → dist/learning-off}/agents/skim.md +1 -8
  47. package/dist/learning-off/agents/triage.md +163 -0
  48. package/dist/learning-off/commands/bug-analysis.md +420 -0
  49. package/dist/learning-off/commands/code-review.md +525 -0
  50. package/dist/learning-off/commands/debug.md +294 -0
  51. package/dist/learning-off/commands/dynamic-build.md +1255 -0
  52. package/dist/learning-off/commands/dynamic-plan.md +424 -0
  53. package/dist/learning-off/commands/dynamic-profile.md +214 -0
  54. package/dist/learning-off/commands/dynamic-tickets.md +632 -0
  55. package/dist/learning-off/commands/explore.md +210 -0
  56. package/dist/learning-off/commands/implement.md +808 -0
  57. package/dist/learning-off/commands/plan.md +664 -0
  58. package/dist/learning-off/commands/release.md +310 -0
  59. package/dist/learning-off/commands/research.md +222 -0
  60. package/dist/learning-off/commands/resolve.md +837 -0
  61. package/dist/learning-off/commands/self-review.md +266 -0
  62. package/dist/skills/git/references/tracker/_contract.md +33 -0
  63. package/dist/skills/git/references/tracker/github/fetch-issue.md +2 -0
  64. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +2 -0
  65. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +4 -0
  66. package/dist/skills/git/references/tracker/github/post-wave-report.md +2 -0
  67. package/dist/skills/git/references/tracker/github/setup-task.md +12 -0
  68. package/dist/skills/git/references/tracker/jira/associate-release.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/fetch-issue.md +2 -0
  70. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +2 -0
  71. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +4 -0
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +2 -0
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +14 -2
  74. package/dist/skills/git/references/tracker/linear/associate-release.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/fetch-issue.md +2 -0
  76. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +2 -0
  77. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +4 -0
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +2 -0
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +14 -2
  80. package/dist/targets/claude-code/installer.js +72 -36
  81. package/dist/targets/claude-code/language-stamp.js +185 -0
  82. package/dist/targets/claude-code/learning-install.js +489 -0
  83. package/package.json +1 -1
  84. package/src/assets/agents/code.mds +339 -0
  85. package/src/assets/agents/design.mds +149 -0
  86. package/src/assets/agents/diagnose.mds +225 -0
  87. package/src/assets/agents/evaluate.md +1 -3
  88. package/src/assets/agents/git.mds +29 -56
  89. package/src/assets/agents/knowledge.mds +125 -0
  90. package/src/assets/agents/research.mds +176 -0
  91. package/src/assets/agents/review.mds +286 -0
  92. package/src/assets/agents/scrutinize.mds +132 -0
  93. package/src/assets/agents/skim.mds +161 -0
  94. package/src/assets/agents/triage.mds +194 -0
  95. package/src/assets/agents/validate.md +8 -6
  96. package/src/assets/commands/_partials/_compliance.mds +5 -4
  97. package/src/assets/commands/_partials/_decisions.mds +31 -0
  98. package/src/assets/commands/_partials/_engine.mds +9 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +25 -12
  100. package/src/assets/commands/_partials/_preamble.mds +33 -9
  101. package/src/assets/commands/_partials/_publication.mds +5 -4
  102. package/src/assets/commands/_partials/_settings.mds +13 -5
  103. package/src/assets/commands/_partials/_wave.mds +8 -0
  104. package/src/assets/commands/bug-analysis.mds +24 -2
  105. package/src/assets/commands/code-review.mds +147 -44
  106. package/src/assets/commands/debug.mds +17 -1
  107. package/src/assets/commands/dynamic-build.mds +33 -2
  108. package/src/assets/commands/dynamic-plan.mds +36 -6
  109. package/src/assets/commands/dynamic-profile.mds +9 -1
  110. package/src/assets/commands/dynamic-tickets.mds +16 -2
  111. package/src/assets/commands/explore.mds +27 -1
  112. package/src/assets/commands/implement.mds +41 -8
  113. package/src/assets/commands/plan.mds +47 -8
  114. package/src/assets/commands/{release.md → release.mds} +27 -24
  115. package/src/assets/commands/research.mds +28 -4
  116. package/src/assets/commands/resolve.mds +43 -2
  117. package/src/assets/commands/self-review.mds +30 -5
  118. package/src/assets/mds/tracker/_contract.mds +72 -0
  119. package/src/assets/mds/tracker/_github.mds +13 -2
  120. package/src/assets/mds/tracker/_jira.mds +17 -5
  121. package/src/assets/mds/tracker/_linear.mds +17 -5
  122. package/src/assets/mds/tracker/_mcp.mds +2 -2
  123. package/src/assets/mds/tracker/_steps.mds +97 -0
  124. package/src/assets/rules/context-economy.md +10 -0
  125. package/src/assets/rules/go.md +1 -0
  126. package/src/assets/rules/java.md +1 -0
  127. package/src/assets/rules/python.md +1 -0
  128. package/src/assets/rules/rust.md +1 -0
  129. package/src/assets/rules/typescript.md +1 -0
  130. package/src/assets/scripts/claude-md-audit.cjs +611 -0
  131. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -2
  132. package/src/assets/scripts/hooks/json-helper.cjs +13 -5
  133. package/src/assets/scripts/hooks/json-parse +34 -10
  134. package/src/assets/scripts/hooks/session-start-context +315 -7
  135. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  136. package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
  137. package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
  138. package/src/assets/skills/quality-gates/SKILL.md +1 -1
@@ -0,0 +1,489 @@
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 { carryAgentOverrides, reapplyAgentMapping } from '../../core/agent-models.js';
74
+ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
75
+ import { readManifest } from '../../core/manifest.js';
76
+ import { getPackageRoot } from '../../core/paths.js';
77
+ import { isProxyEnabled } from '../../core/proxy-state.js';
78
+ import { DEVFLOW_PLUGINS, LEARNING_GATED_SKILLS, installedLanguageFocuses, prefixSkillName, skillsOf, } from '../../core/plugins.js';
79
+ import { copyDirectory, resolveSkillSource } from './installer.js';
80
+ import { LANGUAGE_STAMPED_COMMANDS, stampForConverge } from './language-stamp.js';
81
+ // ── Bounds ─────────────────────────────────────────────────────────────────
82
+ /**
83
+ * The most variant files one kind may carry. The shipped roster is 14 commands and
84
+ * 9 agents; the bound exists so a directory that grew without limit cannot turn the
85
+ * converge into an unbounded loop (every loop has an explicit bound).
86
+ */
87
+ export const MAX_LEARNING_ROSTER = 256;
88
+ /** Deepest skill-directory descent the tree comparison takes. */
89
+ const MAX_SKILL_TREE_DEPTH = 4;
90
+ /** A roster entry is a flat, lowercase, hyphenated markdown basename. */
91
+ const ROSTER_NAME_RE = /^[a-z0-9][a-z0-9-]*\.md$/;
92
+ const KINDS = ['commands', 'agents'];
93
+ // ── Internals ──────────────────────────────────────────────────────────────
94
+ function installedFile(claudeDir, kind, fileName) {
95
+ return path.join(claudeDir, kind, 'devflow', fileName);
96
+ }
97
+ /** The learning-ON source directories for a kind, most-preferred first. */
98
+ function onSourceDirs(kind, root) {
99
+ return kind === 'commands' ? commandSourceDirs(true, root) : agentSourceDirs(root, true);
100
+ }
101
+ /**
102
+ * The roster of one kind: the `.md` files under dist/learning-off/<kind>, sorted.
103
+ *
104
+ * `present` is false when the directory does not exist (the build has not run, or
105
+ * only `build:cli` did), so the caller can say so instead of reporting a converge
106
+ * that had nothing to do.
107
+ */
108
+ async function readRoster(kind, root, warn) {
109
+ let entries;
110
+ try {
111
+ entries = await fs.readdir(learningOffDir(kind, root));
112
+ }
113
+ catch (err) {
114
+ if (err.code === 'ENOENT')
115
+ return { names: [], present: false };
116
+ warn(`learning variants: cannot read the ${kind} variants — ${String(err)}`);
117
+ return { names: [], present: false };
118
+ }
119
+ const names = entries.filter(entry => ROSTER_NAME_RE.test(entry)).sort();
120
+ if (names.length > MAX_LEARNING_ROSTER) {
121
+ warn(`learning variants: ${names.length} ${kind} variants exceed the bound of ${MAX_LEARNING_ROSTER}; converging the first ${MAX_LEARNING_ROSTER}`);
122
+ return { names: names.slice(0, MAX_LEARNING_ROSTER), present: true };
123
+ }
124
+ return { names, present: true };
125
+ }
126
+ async function readIfPresent(file) {
127
+ try {
128
+ return await fs.readFile(file);
129
+ }
130
+ catch (err) {
131
+ return err.code === 'ENOENT' ? 'absent' : 'unreadable';
132
+ }
133
+ }
134
+ /** First of `candidates` that exists, or undefined. Bounded by the candidate list. */
135
+ async function firstReadable(candidates) {
136
+ for (const file of candidates) {
137
+ try {
138
+ return { file, bytes: await fs.readFile(file) };
139
+ }
140
+ catch { /* not here — next directory in preference order */ }
141
+ }
142
+ return undefined;
143
+ }
144
+ /**
145
+ * The bytes a write would install: the variant `source` wearing the state the
146
+ * installed copy carries, so the byte comparison in {@link convergeFile} compares
147
+ * variants and nothing else.
148
+ *
149
+ * D-LANGUAGE-FOCUS-STAMP, composition: the variant on disk carries the language list the
150
+ * install stamped (code-review.md), and the source carries the shipped `(none)`. The stamp is
151
+ * carried from the installed copy into the text this write would install, so the byte
152
+ * comparison stays honest (a stamped copy of the right variant is `unchanged`, not
153
+ * rewritten on every run) and a variant switch never loses or changes the list. A copy with
154
+ * no list to carry (installed before the stamp existed) is stamped from the selection instead.
155
+ *
156
+ * D-AGENT-OVERRIDE-CARRY, composition: an installed agent carries the `model:` and `effort:`
157
+ * that `devflow agents` and the proxy put there, and the source carries the shipped ones.
158
+ * Carrying them keeps the override window shut (no write ever leaves an agent on its shipped
159
+ * model while reapplyAgentMapping has yet to restore it) and keeps a no-op converge
160
+ * write-free, so "N agent(s) rewritten" counts only real variant switches. reapplyAgentMapping
161
+ * stays the authority: it reads agent-models.json after the converge and the carry only agrees
162
+ * with it (see carryAgentOverrides).
163
+ *
164
+ * Pure.
165
+ */
166
+ function withInstalledState(kind, fileName, source, installed, focuses) {
167
+ if (kind === 'agents') {
168
+ const sourceText = source.toString('utf-8');
169
+ const carried = carryAgentOverrides(sourceText, installed.toString('utf-8'));
170
+ return carried === sourceText ? source : Buffer.from(carried, 'utf-8');
171
+ }
172
+ return LANGUAGE_STAMPED_COMMANDS.includes(fileName.replace(/\.md$/, ''))
173
+ ? Buffer.from(stampForConverge(source.toString('utf-8'), installed.toString('utf-8'), focuses), 'utf-8')
174
+ : source;
175
+ }
176
+ /**
177
+ * Converge ONE installed prompt onto the variant the switch wants.
178
+ *
179
+ * Never throws: every failure is a warning and the outcome `failed`, so the next
180
+ * file is still tried.
181
+ */
182
+ async function convergeFile(kind, fileName, opts) {
183
+ const { claudeDir, learning, warn, root, focuses } = opts;
184
+ const target = installedFile(claudeDir, kind, fileName);
185
+ const installed = await readIfPresent(target);
186
+ if (installed === 'absent')
187
+ return 'not-installed';
188
+ if (installed === 'unreadable') {
189
+ warn(`learning variants: cannot read the installed ${kind}/${fileName} (${target}) — left as it is`);
190
+ return 'failed';
191
+ }
192
+ // Learning off: the variant is the roster file itself. Learning on: the normal
193
+ // source order, which is where the learning-on file of that host lives.
194
+ const candidates = learning
195
+ ? onSourceDirs(kind, root).map(dir => path.join(dir, fileName))
196
+ : [path.join(learningOffDir(kind, root), fileName)];
197
+ const source = await firstReadable(candidates);
198
+ if (source === undefined) {
199
+ warn(`learning variants: no source for ${kind}/${fileName} (searched: ${candidates.join(', ')}) — left as it is`);
200
+ return 'failed';
201
+ }
202
+ const wanted = withInstalledState(kind, fileName, source.bytes, installed, focuses);
203
+ // Byte-compared: an installed file already holding the target bytes is not
204
+ // rewritten, so a run that changes nothing writes nothing.
205
+ if (installed.equals(wanted))
206
+ return 'unchanged';
207
+ try {
208
+ // Prompts are UTF-8 text; the atomic writer takes a string and preserves the
209
+ // target's permission mode.
210
+ await writeFileAtomicExclusive(target, wanted.toString('utf-8'));
211
+ }
212
+ catch (err) {
213
+ warn(`learning variants: could not rewrite ${kind}/${fileName} (${target}) — ${String(err)}`);
214
+ return 'failed';
215
+ }
216
+ return 'rewritten';
217
+ }
218
+ /** Every file below `dir` as sorted relative paths; null when anything is not a plain file or directory. */
219
+ async function listTree(dir, depth = 0) {
220
+ if (depth > MAX_SKILL_TREE_DEPTH)
221
+ return null;
222
+ let entries;
223
+ try {
224
+ entries = await fs.readdir(dir, { withFileTypes: true });
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ const out = [];
230
+ for (const entry of entries) {
231
+ if (entry.isDirectory()) {
232
+ const nested = await listTree(path.join(dir, entry.name), depth + 1);
233
+ if (nested === null)
234
+ return null;
235
+ out.push(...nested.map(rel => path.join(entry.name, rel)));
236
+ }
237
+ else if (entry.isFile()) {
238
+ out.push(entry.name);
239
+ }
240
+ else {
241
+ return null;
242
+ }
243
+ }
244
+ return out.sort();
245
+ }
246
+ /** True only when both trees hold the same files with the same bytes. Any error answers false. */
247
+ async function treesIdentical(a, b) {
248
+ const [left, right] = await Promise.all([listTree(a), listTree(b)]);
249
+ if (left === null || right === null || left.length !== right.length)
250
+ return false;
251
+ for (let i = 0; i < left.length; i++) {
252
+ if (left[i] !== right[i])
253
+ return false;
254
+ try {
255
+ const [x, y] = await Promise.all([fs.readFile(path.join(a, left[i])), fs.readFile(path.join(b, right[i]))]);
256
+ if (!x.equals(y))
257
+ return false;
258
+ }
259
+ catch {
260
+ return false;
261
+ }
262
+ }
263
+ return true;
264
+ }
265
+ async function pathExists(p) {
266
+ try {
267
+ await fs.access(p);
268
+ return true;
269
+ }
270
+ catch {
271
+ return false;
272
+ }
273
+ }
274
+ /**
275
+ * Install one skill directory atomically: stage a copy beside the claude dir's
276
+ * skills, then swap it in, restoring the old directory if the swap fails.
277
+ *
278
+ * Staged under `claudeDir` itself, not under `skills/`: a staging directory a
279
+ * crash leaves behind must not look like a second skill to Claude Code.
280
+ */
281
+ async function swapInSkillDirectory(claudeDir, sourceDir, target) {
282
+ const staging = path.join(claudeDir, `.devflow-learning-${process.pid}-${Date.now().toString(36)}.tmp`);
283
+ const backup = `${staging}.old`;
284
+ try {
285
+ await copyDirectory(sourceDir, staging);
286
+ await fs.mkdir(path.dirname(target), { recursive: true });
287
+ const hadTarget = await pathExists(target);
288
+ if (hadTarget)
289
+ await fs.rename(target, backup);
290
+ try {
291
+ await fs.rename(staging, target);
292
+ }
293
+ catch (err) {
294
+ if (hadTarget)
295
+ await fs.rename(backup, target);
296
+ throw err;
297
+ }
298
+ }
299
+ finally {
300
+ await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
301
+ await fs.rm(backup, { recursive: true, force: true }).catch(() => undefined);
302
+ }
303
+ }
304
+ /** Converge the learning-gated skill directories. Never throws. */
305
+ async function convergeSkills(opts) {
306
+ const { claudeDir, devflowDir, learning, plugins, warn } = opts;
307
+ const selected = skillsOf(plugins);
308
+ let state = 'unchanged';
309
+ // Every gated skill is converged: a failure on one must not skip the next.
310
+ for (const skill of LEARNING_GATED_SKILLS) {
311
+ const target = path.join(claudeDir, 'skills', prefixSkillName(skill));
312
+ try {
313
+ if (!learning) {
314
+ if (!(await pathExists(target)))
315
+ continue;
316
+ await fs.rm(target, { recursive: true });
317
+ state = 'removed';
318
+ continue;
319
+ }
320
+ if (!selected.has(skill)) {
321
+ if (state === 'unchanged')
322
+ state = 'not-selected';
323
+ continue;
324
+ }
325
+ const resolved = await resolveSkillSource(skill, devflowDir, opts.packageRoot);
326
+ if ((await pathExists(target)) && (await treesIdentical(resolved.dir, target)))
327
+ continue;
328
+ await swapInSkillDirectory(claudeDir, resolved.dir, target);
329
+ state = 'installed';
330
+ }
331
+ catch (err) {
332
+ warn(`learning variants: could not converge the ${prefixSkillName(skill)} skill (${target}) — ${String(err)}`);
333
+ state = 'failed';
334
+ }
335
+ }
336
+ return state;
337
+ }
338
+ // ── Convergence ────────────────────────────────────────────────────────────
339
+ /**
340
+ * Make the installed commands, agents and apply-decisions skill match the
341
+ * machine's learning switch.
342
+ *
343
+ * Never throws. A caller reads `converged`; it does not catch. A claudeDir that is
344
+ * not absolute is reported the same way rather than thrown, because an install
345
+ * must not die on it.
346
+ */
347
+ export async function convergeLearningVariants(opts) {
348
+ const { claudeDir, learning, warn } = opts;
349
+ const empty = {
350
+ converged: false, commandsRewritten: [], agentsRewritten: [], unchanged: 0, notInstalled: 0, skill: 'unchanged',
351
+ };
352
+ // Precondition, asserted in production code: a relative claudeDir would resolve
353
+ // the install targets somewhere unexpected.
354
+ if (!path.isAbsolute(claudeDir)) {
355
+ warn(`learning variants: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
356
+ return empty;
357
+ }
358
+ const root = opts.packageRoot ?? getPackageRoot();
359
+ const focuses = installedLanguageFocuses(opts.plugins);
360
+ let failed = false;
361
+ let unchanged = 0;
362
+ let notInstalled = 0;
363
+ const rewritten = { commands: [], agents: [] };
364
+ for (const kind of KINDS) {
365
+ const roster = await readRoster(kind, root, warn);
366
+ if (!roster.present) {
367
+ warn(`learning variants: no ${kind} variants under ${learningOffDir(kind, root)} — run \`npm run build:mds\`; installed ${kind} left as they are`);
368
+ failed = true;
369
+ continue;
370
+ }
371
+ for (const fileName of roster.names) {
372
+ const outcome = await convergeFile(kind, fileName, { claudeDir, learning, warn, root, focuses });
373
+ if (outcome === 'rewritten')
374
+ rewritten[kind].push(fileName);
375
+ else if (outcome === 'unchanged')
376
+ unchanged++;
377
+ else if (outcome === 'not-installed')
378
+ notInstalled++;
379
+ else
380
+ failed = true;
381
+ }
382
+ }
383
+ // Evaluated unconditionally, after the files: a failed prompt never skips the skill
384
+ // (one failure must not leave the rest of the fan-out half-converged).
385
+ const skill = await convergeSkills(opts);
386
+ if (skill === 'failed')
387
+ failed = true;
388
+ return {
389
+ converged: !failed,
390
+ commandsRewritten: rewritten.commands,
391
+ agentsRewritten: rewritten.agents,
392
+ unchanged,
393
+ notInstalled,
394
+ skill,
395
+ };
396
+ }
397
+ // ── The toggle ─────────────────────────────────────────────────────────────
398
+ /** The running package's version, or null when its package.json cannot be read or holds none. */
399
+ export async function readRunningVersion(packageRoot = getPackageRoot()) {
400
+ try {
401
+ const pkg = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf-8'));
402
+ return typeof pkg.version === 'string' && pkg.version !== '' ? pkg.version : null;
403
+ }
404
+ catch {
405
+ return null;
406
+ }
407
+ }
408
+ /**
409
+ * One line for what a converge changed, or null when it changed nothing.
410
+ *
411
+ * The caller prints it only on a toggle: a run that moved nothing says nothing.
412
+ */
413
+ export function describeLearningConverge(result, learning) {
414
+ const parts = [];
415
+ const prompts = result.commandsRewritten.length + result.agentsRewritten.length;
416
+ if (prompts > 0) {
417
+ parts.push(`${result.commandsRewritten.length} command(s) and ${result.agentsRewritten.length} agent(s) ` +
418
+ `rewritten for the learning-${learning ? 'on' : 'off'} variant`);
419
+ }
420
+ if (result.skill === 'installed')
421
+ parts.push('the apply-decisions skill installed');
422
+ if (result.skill === 'removed')
423
+ parts.push('the apply-decisions skill removed');
424
+ return parts.length === 0 ? null : parts.join('; ');
425
+ }
426
+ /**
427
+ * The version-skew check, apart from the I/O so it is provable by itself: null when
428
+ * the installed prompts and this package agree, else the message to print.
429
+ */
430
+ export function learningToggleSkew(installedVersion, runningVersion, learning) {
431
+ if (runningVersion !== null && installedVersion === runningVersion)
432
+ return null;
433
+ return (`The installed prompts come from devflow ${installedVersion} and this CLI is ${runningVersion ?? 'of unknown version'}, ` +
434
+ `so the learning-${learning ? 'on' : 'off'} variants were not applied. Run \`devflow init\` to apply them.`);
435
+ }
436
+ /**
437
+ * What `devflow learning --enable/--disable` does after the switch is written:
438
+ * converge the installed tree onto the new value, then reapply the saved agent
439
+ * model and effort mapping.
440
+ *
441
+ * The converge carries each installed agent's model and effort into the variant it
442
+ * writes (D-AGENT-OVERRIDE-CARRY), so the overrides are already on the file and this
443
+ * reapply is the authority behind that carry, not a repair of a default window: it
444
+ * re-reads agent-models.json and corrects any agent whose installed values had
445
+ * drifted from it. It runs only when the converge rewrote an agent, and a failure
446
+ * there is a warning like every other failure here.
447
+ *
448
+ * Never throws.
449
+ */
450
+ export async function applyLearningToggle(opts) {
451
+ const { claudeDir, devflowDir, learning, runningVersion, warn } = opts;
452
+ const manifest = await readManifest(devflowDir);
453
+ if (manifest === null) {
454
+ return {
455
+ kind: 'skipped',
456
+ reason: 'no-manifest',
457
+ message: 'No readable install manifest, so the learning variants were not applied. Run `devflow init`.',
458
+ };
459
+ }
460
+ const skew = learningToggleSkew(manifest.version, runningVersion, learning);
461
+ if (skew !== null)
462
+ return { kind: 'skipped', reason: 'version-skew', message: skew };
463
+ const selection = new Set(manifest.plugins);
464
+ const result = await convergeLearningVariants({
465
+ claudeDir,
466
+ devflowDir,
467
+ learning,
468
+ plugins: DEVFLOW_PLUGINS.filter(plugin => selection.has(plugin.name)),
469
+ warn,
470
+ packageRoot: opts.packageRoot,
471
+ });
472
+ let agentMappingReapplied = false;
473
+ if (result.agentsRewritten.length > 0) {
474
+ try {
475
+ await reapplyAgentMapping({
476
+ proxyEnabled: await isProxyEnabled(devflowDir),
477
+ installDir: path.join(claudeDir, 'agents', 'devflow'),
478
+ devflowDir,
479
+ onWarning: warn,
480
+ });
481
+ agentMappingReapplied = true;
482
+ }
483
+ catch (err) {
484
+ warn(`learning variants: could not reapply the agent model mapping — ${String(err)}`);
485
+ }
486
+ }
487
+ return { kind: 'converged', result, agentMappingReapplied };
488
+ }
489
+ //# sourceMappingURL=learning-install.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-kit",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "A meta-harness for Claude Code — turns a single coding agent into an engineering team: orchestration, parallel review, persistent memory, self-learning, and graph workflows",
5
5
  "type": "module",
6
6
  "bin": {