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.
- package/CHANGELOG.md +47 -0
- package/README.md +8 -4
- package/dist/agents/code-edit.md +133 -0
- package/dist/agents/code-fix.md +165 -0
- package/dist/agents/code.md +187 -0
- package/{src/assets → dist}/agents/design.md +1 -2
- package/{src/assets → dist}/agents/diagnose.md +1 -2
- package/dist/agents/git.md +33 -722
- package/{src/assets → dist}/agents/knowledge.md +12 -5
- package/{src/assets → dist}/agents/research.md +8 -2
- package/{src/assets → dist}/agents/review.md +8 -11
- package/{src/assets → dist}/agents/scrutinize.md +4 -1
- package/dist/agents/skim.md +152 -0
- package/{src/assets → dist}/agents/triage.md +9 -2
- package/dist/cli/commands/behavior-prompts.js +77 -0
- package/dist/cli/commands/behavior.js +123 -0
- package/dist/cli/commands/flags.js +8 -2
- package/dist/cli/commands/init-seed.js +7 -3
- package/dist/cli/commands/init.js +134 -5
- package/dist/cli/commands/learning.js +38 -3
- package/dist/cli/commands/rules.js +16 -0
- package/dist/cli/commands/tracker.js +112 -15
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +35 -13
- package/dist/commands/code-review.md +155 -69
- package/dist/commands/debug.md +39 -16
- package/dist/commands/dynamic-build.md +61 -72
- package/dist/commands/dynamic-plan.md +39 -25
- package/dist/commands/dynamic-profile.md +28 -14
- package/dist/commands/dynamic-tickets.md +38 -22
- package/dist/commands/explore.md +18 -16
- package/dist/commands/implement.md +121 -150
- package/dist/commands/plan.md +51 -31
- package/dist/commands/release.md +72 -6
- package/dist/commands/research.md +49 -20
- package/dist/commands/resolve.md +54 -46
- package/dist/commands/self-review.md +38 -25
- package/dist/core/agent-frontmatter.js +101 -0
- 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/compliance-compose.js +6 -4
- package/dist/core/feature-switch.js +41 -1
- package/dist/core/flags.js +139 -0
- package/dist/core/fs-atomic.js +8 -3
- package/dist/core/learning-variants.js +213 -0
- package/dist/core/manifest.js +73 -3
- package/dist/core/mds-variants.js +98 -19
- package/dist/core/plugins.js +111 -72
- package/dist/core/project-paths.js +0 -4
- package/dist/core/proxy-log.js +1 -1
- package/dist/core/proxy-state.js +1 -1
- package/dist/core/tracker.js +120 -2
- package/dist/learning-off/agents/code-edit.md +129 -0
- package/dist/learning-off/agents/code-fix.md +161 -0
- package/dist/learning-off/agents/code.md +183 -0
- package/dist/learning-off/agents/design.md +118 -0
- package/dist/learning-off/agents/diagnose.md +210 -0
- package/dist/learning-off/agents/knowledge.md +96 -0
- package/dist/learning-off/agents/research.md +155 -0
- package/dist/learning-off/agents/review.md +224 -0
- package/dist/learning-off/agents/scrutinize.md +120 -0
- package/{src/assets → dist/learning-off}/agents/skim.md +5 -8
- package/dist/learning-off/agents/triage.md +170 -0
- package/dist/learning-off/commands/bug-analysis.md +420 -0
- package/dist/learning-off/commands/code-review.md +530 -0
- package/dist/learning-off/commands/debug.md +314 -0
- package/dist/learning-off/commands/dynamic-build.md +1245 -0
- package/dist/learning-off/commands/dynamic-plan.md +426 -0
- package/dist/learning-off/commands/dynamic-profile.md +214 -0
- package/dist/learning-off/commands/dynamic-tickets.md +634 -0
- package/dist/learning-off/commands/explore.md +210 -0
- package/dist/learning-off/commands/implement.md +774 -0
- package/dist/learning-off/commands/plan.md +671 -0
- package/dist/learning-off/commands/release.md +311 -0
- package/dist/learning-off/commands/research.md +229 -0
- package/dist/learning-off/commands/resolve.md +842 -0
- package/dist/learning-off/commands/self-review.md +266 -0
- package/dist/skills/git/references/learn-conventions.md +27 -3
- package/dist/skills/git/references/pr/check-ci-status.md +19 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
- package/dist/skills/git/references/pr/post-review-summary.md +20 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
- package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
- package/dist/skills/git/references/pr/validate-branch.md +26 -0
- package/dist/skills/git/references/tracker/_contract.md +33 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/github/create-release.md +30 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/github/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/github/setup-task.md +53 -1
- package/dist/skills/git/references/tracker/jira/associate-release.md +25 -1
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +55 -3
- package/dist/skills/git/references/tracker/linear/associate-release.md +25 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +38 -1
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +49 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +29 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +17 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +55 -3
- package/dist/targets/claude-code/agent-type-owner.js +164 -0
- package/dist/targets/claude-code/behavior-install.js +111 -0
- package/dist/targets/claude-code/installer.js +140 -37
- package/dist/targets/claude-code/language-stamp.js +185 -0
- package/dist/targets/claude-code/learning-install.js +497 -0
- package/dist/targets/claude-code/tracker-install.js +33 -16
- package/dist/targets/claude-code/tracker-servers.js +107 -0
- package/package.json +4 -3
- package/src/assets/agents/_partials/_code.mds +109 -0
- package/src/assets/agents/code-edit.mds +81 -0
- package/src/assets/agents/code-fix.mds +113 -0
- package/src/assets/agents/code.mds +135 -0
- package/src/assets/agents/design.mds +148 -0
- package/src/assets/agents/diagnose.mds +225 -0
- package/src/assets/agents/evaluate.md +8 -4
- package/src/assets/agents/explore.md +58 -0
- package/src/assets/agents/git.mds +33 -722
- package/src/assets/agents/knowledge.mds +131 -0
- package/src/assets/agents/learning.md +19 -69
- package/src/assets/agents/research.mds +182 -0
- package/src/assets/agents/review.mds +282 -0
- package/src/assets/agents/scrutinize.mds +135 -0
- package/src/assets/agents/simplify.md +3 -0
- package/src/assets/agents/skim.mds +165 -0
- package/src/assets/agents/synthesize.md +26 -352
- package/src/assets/agents/test.md +8 -0
- package/src/assets/agents/tracker.md +20 -7
- package/src/assets/agents/triage.mds +201 -0
- package/src/assets/agents/validate.md +13 -6
- package/src/assets/commands/_partials/_compliance.mds +41 -4
- package/src/assets/commands/_partials/_decisions.mds +33 -2
- package/src/assets/commands/_partials/_engine.mds +14 -6
- package/src/assets/commands/_partials/_knowledge.mds +25 -12
- package/src/assets/commands/_partials/_preamble.mds +34 -10
- package/src/assets/commands/_partials/_publication.mds +5 -4
- package/src/assets/commands/_partials/_roster.mds +4 -3
- package/src/assets/commands/_partials/_settings.mds +13 -5
- package/src/assets/commands/_partials/_wave.mds +9 -1
- package/src/assets/commands/bug-analysis.mds +27 -5
- package/src/assets/commands/code-review.mds +158 -52
- package/src/assets/commands/debug.mds +41 -3
- package/src/assets/commands/dynamic-build.mds +47 -27
- package/src/assets/commands/dynamic-plan.mds +40 -9
- package/src/assets/commands/dynamic-profile.mds +9 -1
- package/src/assets/commands/dynamic-tickets.mds +21 -6
- package/src/assets/commands/explore.mds +28 -2
- package/src/assets/commands/implement.mds +126 -129
- package/src/assets/commands/plan.mds +58 -14
- package/src/assets/commands/{release.md → release.mds} +30 -26
- package/src/assets/commands/research.mds +42 -11
- package/src/assets/commands/resolve.mds +59 -15
- package/src/assets/commands/self-review.mds +30 -5
- package/src/assets/mds/.mdsroot +4 -0
- package/src/assets/mds/git/_ops.mds +383 -0
- package/src/assets/mds/git/_ops_pr.mds +201 -0
- package/src/assets/mds/git/_ops_steps.mds +147 -0
- package/src/assets/mds/git/_pr.mds +39 -44
- package/src/assets/mds/git/_references.mds +6 -3
- package/src/assets/mds/tracker/_common.mds +5 -5
- package/src/assets/mds/tracker/_contract.mds +73 -0
- package/src/assets/mds/tracker/_github.mds +73 -16
- package/src/assets/mds/tracker/_jira.mds +77 -19
- package/src/assets/mds/tracker/_linear.mds +77 -19
- package/src/assets/mds/tracker/_mcp.mds +2 -2
- package/src/assets/mds/tracker/_steps.mds +98 -0
- package/src/assets/rules/behavior.md +6 -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 +3 -3
- package/src/assets/scripts/hooks/json-helper.cjs +13 -5
- package/src/assets/scripts/hooks/json-parse +34 -10
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
- package/src/assets/scripts/hooks/session-start-context +315 -7
- package/src/assets/skills/agent-references/SKILL.md +33 -0
- package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
- package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
- package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
- package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
- package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
- 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/compliance/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +80 -83
- package/src/assets/skills/docs-framework/references/patterns.md +14 -25
- package/src/assets/skills/docs-framework/references/violations.md +7 -7
- package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
- package/src/assets/skills/gap-analysis/SKILL.md +0 -16
- package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
- 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
|