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.
- package/CHANGELOG.md +18 -0
- package/dist/agents/code.md +330 -0
- package/{src/assets → dist}/agents/design.md +1 -1
- package/{src/assets → dist}/agents/diagnose.md +1 -2
- package/dist/agents/git.md +29 -56
- package/{src/assets → dist}/agents/knowledge.md +4 -3
- package/{src/assets → dist}/agents/research.md +2 -2
- package/{src/assets → dist}/agents/review.md +8 -7
- package/{src/assets → dist}/agents/scrutinize.md +1 -1
- package/dist/agents/skim.md +148 -0
- package/{src/assets → dist}/agents/triage.md +1 -1
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/learning.js +38 -3
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/commands/bug-analysis.md +30 -8
- package/dist/commands/code-review.md +141 -60
- package/dist/commands/debug.md +14 -12
- package/dist/commands/dynamic-build.md +37 -38
- package/dist/commands/dynamic-plan.md +30 -18
- package/dist/commands/dynamic-profile.md +27 -13
- package/dist/commands/dynamic-tickets.md +28 -14
- package/dist/commands/explore.md +15 -13
- package/dist/commands/implement.md +33 -28
- package/dist/commands/plan.md +37 -24
- package/dist/commands/release.md +69 -4
- package/dist/commands/research.md +33 -11
- package/dist/commands/resolve.md +35 -32
- package/dist/commands/self-review.md +36 -23
- package/dist/core/agent-models.js +43 -0
- package/dist/core/assets.js +55 -10
- package/dist/core/claude-md-audit.js +190 -0
- package/dist/core/feature-switch.js +20 -1
- package/dist/core/flags.js +28 -0
- package/dist/core/fs-atomic.js +8 -3
- package/dist/core/learning-variants.js +213 -0
- package/dist/core/manifest.js +62 -0
- package/dist/core/mds-variants.js +38 -1
- package/dist/core/plugins.js +71 -9
- package/{src/assets → dist/learning-off}/agents/code.md +6 -10
- package/dist/learning-off/agents/design.md +119 -0
- package/dist/learning-off/agents/diagnose.md +210 -0
- package/dist/learning-off/agents/knowledge.md +90 -0
- package/dist/learning-off/agents/research.md +149 -0
- package/dist/learning-off/agents/review.md +228 -0
- package/dist/learning-off/agents/scrutinize.md +117 -0
- package/{src/assets → dist/learning-off}/agents/skim.md +1 -8
- package/dist/learning-off/agents/triage.md +163 -0
- package/dist/learning-off/commands/bug-analysis.md +420 -0
- package/dist/learning-off/commands/code-review.md +525 -0
- package/dist/learning-off/commands/debug.md +294 -0
- package/dist/learning-off/commands/dynamic-build.md +1255 -0
- package/dist/learning-off/commands/dynamic-plan.md +424 -0
- package/dist/learning-off/commands/dynamic-profile.md +214 -0
- package/dist/learning-off/commands/dynamic-tickets.md +632 -0
- package/dist/learning-off/commands/explore.md +210 -0
- package/dist/learning-off/commands/implement.md +808 -0
- package/dist/learning-off/commands/plan.md +664 -0
- package/dist/learning-off/commands/release.md +310 -0
- package/dist/learning-off/commands/research.md +222 -0
- package/dist/learning-off/commands/resolve.md +837 -0
- package/dist/learning-off/commands/self-review.md +266 -0
- package/dist/skills/git/references/tracker/_contract.md +33 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +12 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +14 -2
- package/dist/skills/git/references/tracker/linear/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +4 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +14 -2
- package/dist/targets/claude-code/installer.js +72 -36
- package/dist/targets/claude-code/language-stamp.js +185 -0
- package/dist/targets/claude-code/learning-install.js +489 -0
- package/package.json +1 -1
- package/src/assets/agents/code.mds +339 -0
- package/src/assets/agents/design.mds +149 -0
- package/src/assets/agents/diagnose.mds +225 -0
- package/src/assets/agents/evaluate.md +1 -3
- package/src/assets/agents/git.mds +29 -56
- package/src/assets/agents/knowledge.mds +125 -0
- package/src/assets/agents/research.mds +176 -0
- package/src/assets/agents/review.mds +286 -0
- package/src/assets/agents/scrutinize.mds +132 -0
- package/src/assets/agents/skim.mds +161 -0
- package/src/assets/agents/triage.mds +194 -0
- package/src/assets/agents/validate.md +8 -6
- package/src/assets/commands/_partials/_compliance.mds +5 -4
- package/src/assets/commands/_partials/_decisions.mds +31 -0
- package/src/assets/commands/_partials/_engine.mds +9 -1
- package/src/assets/commands/_partials/_knowledge.mds +25 -12
- package/src/assets/commands/_partials/_preamble.mds +33 -9
- package/src/assets/commands/_partials/_publication.mds +5 -4
- package/src/assets/commands/_partials/_settings.mds +13 -5
- package/src/assets/commands/_partials/_wave.mds +8 -0
- package/src/assets/commands/bug-analysis.mds +24 -2
- package/src/assets/commands/code-review.mds +147 -44
- package/src/assets/commands/debug.mds +17 -1
- package/src/assets/commands/dynamic-build.mds +33 -2
- package/src/assets/commands/dynamic-plan.mds +36 -6
- package/src/assets/commands/dynamic-profile.mds +9 -1
- package/src/assets/commands/dynamic-tickets.mds +16 -2
- package/src/assets/commands/explore.mds +27 -1
- package/src/assets/commands/implement.mds +41 -8
- package/src/assets/commands/plan.mds +47 -8
- package/src/assets/commands/{release.md → release.mds} +27 -24
- package/src/assets/commands/research.mds +28 -4
- package/src/assets/commands/resolve.mds +43 -2
- package/src/assets/commands/self-review.mds +30 -5
- package/src/assets/mds/tracker/_contract.mds +72 -0
- package/src/assets/mds/tracker/_github.mds +13 -2
- package/src/assets/mds/tracker/_jira.mds +17 -5
- package/src/assets/mds/tracker/_linear.mds +17 -5
- package/src/assets/mds/tracker/_mcp.mds +2 -2
- package/src/assets/mds/tracker/_steps.mds +97 -0
- package/src/assets/rules/context-economy.md +10 -0
- package/src/assets/rules/go.md +1 -0
- package/src/assets/rules/java.md +1 -0
- package/src/assets/rules/python.md +1 -0
- package/src/assets/rules/rust.md +1 -0
- package/src/assets/rules/typescript.md +1 -0
- package/src/assets/scripts/claude-md-audit.cjs +611 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -2
- package/src/assets/scripts/hooks/json-helper.cjs +13 -5
- package/src/assets/scripts/hooks/json-parse +34 -10
- package/src/assets/scripts/hooks/session-start-context +315 -7
- package/src/assets/skills/apply-decisions/SKILL.md +1 -1
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
- package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
- 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
|
+
"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": {
|