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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { SKILL_NAMESPACE, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan } from '../../core/plugins.js';
|
|
5
|
-
import { skillsDir, agentSourceDirs, rulesDir,
|
|
4
|
+
import { SKILL_NAMESPACE, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan, omitLearningGatedSkills } from '../../core/plugins.js';
|
|
5
|
+
import { skillsDir, agentSourceDirs, rulesDir, commandSourceDirs, scriptsDir, compiledSkillRefsDir } from '../../core/assets.js';
|
|
6
6
|
import { getPackageRoot, isContainedIn } from '../../core/paths.js';
|
|
7
7
|
import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
|
|
8
8
|
import { generatedReferenceManifest, installedReferenceManifest, PR_HOST_DESTINATION_ROOT, SKILL_REFS_SKILL_NAME, TRACKER_DESTINATION_ROOT } from '../../core/mds-variants.js';
|
|
9
9
|
import { sweepOrphanedReferences, MAX_REFERENCE_SWEEP_DEPTH } from '../../core/reference-sweep.js';
|
|
10
10
|
import { TRACKER_AGENT_NAME } from './tracker-install.js';
|
|
11
|
+
import { restampInstalledCommands } from './language-stamp.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Shadow validation helpers (exported — reused by Step 4 list commands)
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -1278,6 +1279,39 @@ async function firstExisting(candidates) {
|
|
|
1278
1279
|
}
|
|
1279
1280
|
return undefined;
|
|
1280
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Resolve the directory a registry skill installs from: its valid shadow, else
|
|
1284
|
+
* the shipped source (an invalid shadow is reported and the source is
|
|
1285
|
+
* installed in its place).
|
|
1286
|
+
*
|
|
1287
|
+
* The ONE spelling of that decision: the install loop and
|
|
1288
|
+
* convergeLearningVariants both call it, so a skill the learning toggle installs
|
|
1289
|
+
* later is shadowed exactly as one the install installed.
|
|
1290
|
+
*
|
|
1291
|
+
* The shipped source is stat-checked FIRST, even under a valid shadow, so an
|
|
1292
|
+
* absent source is a packaging failure that throws (the hard-error policy for a
|
|
1293
|
+
* declared source) rather than being masked by a shadow. Callers that must not
|
|
1294
|
+
* throw catch it.
|
|
1295
|
+
*
|
|
1296
|
+
* @param packageRoot - Package root the shipped source is read from. Injectable so
|
|
1297
|
+
* a caller working on a temp tree resolves the skill from the same root as its
|
|
1298
|
+
* other sources; omitted, it is the running package.
|
|
1299
|
+
*/
|
|
1300
|
+
export async function resolveSkillSource(skillName, devflowDir, packageRoot) {
|
|
1301
|
+
const skillSource = path.join(skillsDir(packageRoot), skillName);
|
|
1302
|
+
let isDir = false;
|
|
1303
|
+
try {
|
|
1304
|
+
isDir = (await fs.stat(skillSource)).isDirectory();
|
|
1305
|
+
}
|
|
1306
|
+
catch { /* stat failed — source absent */ }
|
|
1307
|
+
if (!isDir) {
|
|
1308
|
+
throw new Error(`Skill source not found for declared skill "${skillName}": ${skillSource}. ` +
|
|
1309
|
+
`Ensure the skill directory exists in src/assets/skills/.`);
|
|
1310
|
+
}
|
|
1311
|
+
const shadowDir = path.join(devflowDir, 'skills', skillName);
|
|
1312
|
+
const shadow = await validateSkillShadow(shadowDir);
|
|
1313
|
+
return { dir: shadow === 'valid' ? shadowDir : skillSource, shadow };
|
|
1314
|
+
}
|
|
1281
1315
|
/**
|
|
1282
1316
|
* Registry skills that have a shadow directory under `~/.devflow/skills/`.
|
|
1283
1317
|
*
|
|
@@ -1347,7 +1381,7 @@ async function removeDeselectedSkills(skillsDir, names) {
|
|
|
1347
1381
|
* Returns an InstallReport describing which shadows were applied and which were skipped.
|
|
1348
1382
|
*/
|
|
1349
1383
|
export async function installViaFileCopy(options) {
|
|
1350
|
-
const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, warn = () => { }, } = options;
|
|
1384
|
+
const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, learning, warn = () => { }, } = options;
|
|
1351
1385
|
const report = {
|
|
1352
1386
|
shadowedSkills: [],
|
|
1353
1387
|
shadowedRules: [],
|
|
@@ -1484,11 +1518,13 @@ export async function installViaFileCopy(options) {
|
|
|
1484
1518
|
warn(`Could not remove deselected skill "${prefixSkillName(failure.name)}" — ${String(failure.error)}`);
|
|
1485
1519
|
}
|
|
1486
1520
|
// Install commands from selected plugins using registry-driven lookup.
|
|
1487
|
-
// Source: dist/commands/{name}.md
|
|
1521
|
+
// Source: dist/commands/{name}.md, or — with learning off — the learning-off
|
|
1522
|
+
// variant dist/learning-off/commands/{name}.md when the host has one
|
|
1523
|
+
// (D-LEARNING-VARIANT-INSTALL; commandSourceDirs owns the order).
|
|
1488
1524
|
// A declared command with no compiled source file is a hard error, not a skip.
|
|
1489
1525
|
spinner.message('Installing commands and agents...');
|
|
1490
1526
|
const commandsTarget = path.join(claudeDir, 'commands', 'devflow');
|
|
1491
|
-
const
|
|
1527
|
+
const commandDirs = commandSourceDirs(learning);
|
|
1492
1528
|
const commandsSourceNames = new Set();
|
|
1493
1529
|
for (const plugin of plugins) {
|
|
1494
1530
|
for (const cmd of plugin.commands) {
|
|
@@ -1499,12 +1535,11 @@ export async function installViaFileCopy(options) {
|
|
|
1499
1535
|
if (commandsSourceNames.size > 0) {
|
|
1500
1536
|
await fs.mkdir(commandsTarget, { recursive: true });
|
|
1501
1537
|
for (const name of commandsSourceNames) {
|
|
1502
|
-
const srcFile = path.join(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
throw new Error(`Command source not found for declared command "${name}": ${srcFile}. ` +
|
|
1538
|
+
const srcFile = await firstExisting(commandDirs.map(dir => path.join(dir, mdFileName(name))));
|
|
1539
|
+
if (srcFile === undefined) {
|
|
1540
|
+
// The error names the learning-ON path: that file exists for every declared
|
|
1541
|
+
// command, while a learning-off file exists only for a host with an arm.
|
|
1542
|
+
throw new Error(`Command source not found for declared command "${name}": ${path.join(commandSourceDirs(true)[0], mdFileName(name))}. ` +
|
|
1508
1543
|
`Ensure build:mds ran successfully before install.`);
|
|
1509
1544
|
}
|
|
1510
1545
|
await fs.copyFile(srcFile, path.join(commandsTarget, mdFileName(name)));
|
|
@@ -1514,6 +1549,18 @@ export async function installViaFileCopy(options) {
|
|
|
1514
1549
|
// knownNames spans ALL plugins (getAllCommandNames) so commands from uninstalled
|
|
1515
1550
|
// plugins survive a partial run. Only names absent from the full registry are removed.
|
|
1516
1551
|
recordSweep(report, 'command', await sweepOrphanedAssets(commandsTarget, new Set(getAllCommandNames()), mdEntryName));
|
|
1552
|
+
// D-LANGUAGE-FOCUS-STAMP: record the language focuses the EFFECTIVE selection installs on the
|
|
1553
|
+
// installed /code-review, on every install shape. This is the language gate the command reads
|
|
1554
|
+
// (no run-time probe of Claude Code's directory), so it runs after the copy and the sweep and
|
|
1555
|
+
// whether or not code-review's own plugin is in this run: `--plugin=devflow-typescript` copies no
|
|
1556
|
+
// command, yet must update a code-review an earlier run installed. The copy above wrote the shipped
|
|
1557
|
+
// `(none)` line; the rewrite compares against what is on disk now, so neither that copy nor the
|
|
1558
|
+
// pre-clean can defeat it. A failure is a warning, never an abort.
|
|
1559
|
+
await restampInstalledCommands({
|
|
1560
|
+
claudeDir,
|
|
1561
|
+
effectivePlugins: options.effectivePlugins ?? plugins,
|
|
1562
|
+
warn,
|
|
1563
|
+
});
|
|
1517
1564
|
// Install agents (deduplicated), resolved dist-first with a src fallback:
|
|
1518
1565
|
// dist/agents/{name}.md (compiled from an .mds generator host) wins over
|
|
1519
1566
|
// src/assets/agents/{name}.md. A declared agent absent from BOTH is a
|
|
@@ -1532,7 +1579,8 @@ export async function installViaFileCopy(options) {
|
|
|
1532
1579
|
// registry via getAllAgentNames() — still treats a converged tracker.md as known
|
|
1533
1580
|
// and leaves it alone.
|
|
1534
1581
|
const agentsTarget = path.join(claudeDir, 'agents', 'devflow');
|
|
1535
|
-
|
|
1582
|
+
// Learning-aware (D-LEARNING-VARIANT-INSTALL). An injected list wins as given.
|
|
1583
|
+
const agentDirs = options.agentSourceDirs ?? agentSourceDirs(undefined, learning);
|
|
1536
1584
|
const allAgentNames = new Set();
|
|
1537
1585
|
for (const plugin of plugins) {
|
|
1538
1586
|
for (const agent of plugin.agents) {
|
|
@@ -1549,7 +1597,7 @@ export async function installViaFileCopy(options) {
|
|
|
1549
1597
|
const candidates = agentDirs.map(dir => path.join(dir, mdFileName(agentName)));
|
|
1550
1598
|
const srcFile = await firstExisting(candidates);
|
|
1551
1599
|
if (srcFile === undefined) {
|
|
1552
|
-
throw new Error(`Agent source not found for declared agent "${agentName}": ${
|
|
1600
|
+
throw new Error(`Agent source not found for declared agent "${agentName}": ${path.join(options.agentSourceDirs?.[0] ?? agentSourceDirs()[0], mdFileName(agentName))}. ` +
|
|
1553
1601
|
`Run \`npm run build:mds\` if it is compiled from an .mds generator host, otherwise ` +
|
|
1554
1602
|
`ensure the agent file exists in src/assets/agents/ (searched: ${candidates.join(', ')}).`);
|
|
1555
1603
|
}
|
|
@@ -1564,33 +1612,21 @@ export async function installViaFileCopy(options) {
|
|
|
1564
1612
|
// Resolved from flat src/assets/skills/{name}/ (no per-plugin subdirectory).
|
|
1565
1613
|
// A declared skill whose source directory is absent is a build/packaging failure
|
|
1566
1614
|
// and throws rather than silently skipping (matches command pattern).
|
|
1615
|
+
//
|
|
1616
|
+
// D-LEARNING-VARIANT-INSTALL: a learning-off machine skips the learning-gated
|
|
1617
|
+
// skills here. The pre-clean above walked the UNFILTERED map, so on a full
|
|
1618
|
+
// install a leftover copy is already gone; a partial install leaves removal to
|
|
1619
|
+
// convergeLearningVariants, which init runs straight after this call.
|
|
1567
1620
|
spinner.message('Installing skills...');
|
|
1568
|
-
for (const [skillName] of skillsMap) {
|
|
1569
|
-
const
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
isDir = stat.isDirectory();
|
|
1574
|
-
}
|
|
1575
|
-
catch { /* stat failed — source absent */ }
|
|
1576
|
-
if (!isDir) {
|
|
1577
|
-
throw new Error(`Skill source not found for declared skill "${skillName}": ${skillSource}. ` +
|
|
1578
|
-
`Ensure the skill directory exists in src/assets/skills/.`);
|
|
1579
|
-
}
|
|
1580
|
-
const shadowDir = path.join(devflowDir, 'skills', skillName);
|
|
1581
|
-
const prefixedName = prefixSkillName(skillName);
|
|
1582
|
-
const skillTarget = path.join(claudeDir, 'skills', prefixedName);
|
|
1583
|
-
const shadowState = await validateSkillShadow(shadowDir);
|
|
1584
|
-
if (shadowState === 'valid') {
|
|
1585
|
-
await copyDirectory(shadowDir, skillTarget);
|
|
1621
|
+
for (const [skillName] of omitLearningGatedSkills(skillsMap, learning)) {
|
|
1622
|
+
const resolved = await resolveSkillSource(skillName, devflowDir);
|
|
1623
|
+
const skillTarget = path.join(claudeDir, 'skills', prefixSkillName(skillName));
|
|
1624
|
+
await copyDirectory(resolved.dir, skillTarget);
|
|
1625
|
+
if (resolved.shadow === 'valid') {
|
|
1586
1626
|
report.shadowedSkills.push(skillName);
|
|
1587
1627
|
}
|
|
1588
|
-
else if (
|
|
1628
|
+
else if (resolved.shadow === 'missing-skill-md') {
|
|
1589
1629
|
report.skippedShadows.push({ kind: 'skill', name: skillName, reason: 'missing-skill-md' });
|
|
1590
|
-
await copyDirectory(skillSource, skillTarget);
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
await copyDirectory(skillSource, skillTarget);
|
|
1594
1630
|
}
|
|
1595
1631
|
// Converge the generated references onto the skill that was just installed. One call
|
|
1596
1632
|
// site downstream of all three branches above, so a shadowed devflow:git receives the
|
|
@@ -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
|