devflow-kit 3.9.0 → 3.11.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 +142 -0
- package/README.md +1 -1
- package/dist/agents/code.md +6 -6
- package/dist/agents/fix.md +6 -6
- package/dist/agents/harden.md +7 -6
- package/dist/agents/refactor.md +6 -6
- package/dist/cli/commands/ambient.js +10 -2
- package/dist/cli/commands/backup-session.js +131 -0
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +184 -24
- package/dist/cli/commands/curation.js +23 -10
- package/dist/cli/commands/gate-level-prompts.js +39 -25
- package/dist/cli/commands/gates.js +5 -7
- package/dist/cli/commands/init.js +169 -184
- package/dist/cli/commands/install-report.js +129 -0
- package/dist/cli/commands/legacy-sweep.js +159 -0
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +75 -21
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +339 -75
- package/dist/commands/bug-analysis.md +69 -120
- package/dist/commands/dynamic-build.md +23 -23
- package/dist/commands/implement.md +46 -32
- package/dist/commands/release.md +1 -1
- package/dist/commands/resolve.md +35 -31
- package/dist/commands/self-review.md +8 -7
- package/dist/compliance-off/agents/git.md +120 -0
- package/dist/compliance-off/agents/triage.md +180 -0
- package/dist/compliance-off/commands/bug-analysis.md +398 -0
- package/dist/compliance-off/commands/code-review.md +520 -0
- package/dist/compliance-off/commands/dynamic-build.md +1251 -0
- package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
- package/dist/compliance-off/commands/implement.md +736 -0
- package/dist/compliance-off/commands/plan.md +671 -0
- package/dist/compliance-off/commands/release.md +309 -0
- package/dist/compliance-off/commands/resolve.md +742 -0
- package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
- package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
- package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
- package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
- package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
- package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
- package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
- package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
- package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
- package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
- package/dist/core/agent-models.js +19 -4
- package/dist/core/assets.js +126 -17
- package/dist/core/compliance-off-files.js +27 -0
- package/dist/core/compliance.js +14 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/flags.js +2 -2
- package/dist/core/fs-atomic.js +5 -1
- package/dist/core/installed-variants.js +63 -0
- package/dist/core/manifest.js +48 -5
- package/dist/core/mds-variants.js +700 -3
- package/dist/core/orphan-sweep.js +29 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +68 -10
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/curation-compliance-off/agents/triage.md +176 -0
- package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
- package/dist/curation-compliance-off/commands/code-review.md +481 -0
- package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
- package/dist/curation-compliance-off/commands/implement.md +689 -0
- package/dist/curation-compliance-off/commands/plan.md +631 -0
- package/dist/curation-compliance-off/commands/release.md +279 -0
- package/dist/curation-compliance-off/commands/resolve.md +692 -0
- package/dist/curation-off/agents/code.md +6 -6
- package/dist/curation-off/agents/fix.md +6 -6
- package/dist/curation-off/agents/harden.md +7 -6
- package/dist/curation-off/agents/refactor.md +6 -6
- package/dist/curation-off/commands/bug-analysis.md +69 -119
- package/dist/curation-off/commands/dynamic-build.md +23 -23
- package/dist/curation-off/commands/implement.md +46 -32
- package/dist/curation-off/commands/release.md +1 -1
- package/dist/curation-off/commands/resolve.md +35 -31
- package/dist/curation-off/commands/self-review.md +8 -7
- package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
- package/dist/targets/claude-code/behavior-install.js +23 -9
- package/dist/targets/claude-code/charter-render.js +374 -0
- package/dist/targets/claude-code/compliance-install.js +62 -17
- package/dist/targets/claude-code/compliance-off-files.js +249 -0
- package/dist/targets/claude-code/curation-install.js +632 -117
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +795 -199
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +176 -0
- package/dist/targets/claude-code/reference-converge.js +131 -0
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -0
- package/dist/targets/claude-code/tracker-install.js +21 -8
- package/dist/targets/claude-code/tracker-servers.js +20 -6
- package/dist/targets/claude-code/workflow-folder-install.js +251 -0
- package/dist/variant-axes.json +109 -0
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +6 -6
- package/src/assets/agents/evaluate.md +1 -1
- package/src/assets/agents/git.mds +32 -0
- package/src/assets/agents/harden.mds +7 -6
- package/src/assets/agents/test.md +21 -12
- package/src/assets/agents/triage.mds +2 -0
- package/src/assets/agents/validate.md +10 -10
- package/src/assets/commands/_partials/_compliance.mds +5 -3
- package/src/assets/commands/_partials/_engine.mds +9 -8
- package/src/assets/commands/_partials/_publication.mds +10 -1
- package/src/assets/commands/bug-analysis.mds +80 -126
- package/src/assets/commands/code-review.mds +43 -0
- package/src/assets/commands/dynamic-build.mds +109 -15
- package/src/assets/commands/dynamic-tickets.mds +13 -0
- package/src/assets/commands/implement.mds +145 -33
- package/src/assets/commands/plan.mds +19 -0
- package/src/assets/commands/release.mds +50 -1
- package/src/assets/commands/resolve.mds +217 -32
- package/src/assets/commands/self-review.mds +8 -7
- package/src/assets/mds/git/_ops.mds +23 -0
- package/src/assets/mds/git/_ops_steps.mds +17 -0
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/mds/git/_references.mds +6 -0
- package/src/assets/mds/tracker/_common.mds +4 -0
- package/src/assets/mds/tracker/_github.mds +42 -0
- package/src/assets/mds/tracker/_jira.mds +43 -0
- package/src/assets/mds/tracker/_linear.mds +43 -0
- package/src/assets/rules/quality.md +2 -0
- package/src/assets/scripts/ci-wait.cjs +204 -126
- package/src/assets/scripts/hooks/assets/implement-installed +1 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
- package/src/assets/scripts/hooks/preamble +18 -8
- package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
- package/src/assets/scripts/pr-evidence.cjs +41 -2
- package/src/assets/scripts/verify-evidence.cjs +408 -56
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
- package/src/assets/skills/docs-framework/SKILL.md +5 -2
- package/src/assets/skills/git/SKILL.md +3 -3
- package/src/assets/skills/git/references/github-api.md +2 -139
- package/src/assets/skills/git/references/github-posting.md +158 -0
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +1 -1
- package/src/assets/skills/review-methodology/references/report-template.md +1 -2
- package/src/assets/skills/testing/SKILL.md +2 -4
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import color from 'picocolors';
|
|
11
11
|
import { overlayUnitLabel } from '../../targets/claude-code/installer.js';
|
|
12
|
+
import { escapeName, formatCappedList } from '../../targets/claude-code/install-backup.js';
|
|
13
|
+
import { refusedFolderMessage, replacedLinkMessage } from '../../targets/claude-code/linked-folder.js';
|
|
12
14
|
import { SKILL_REFS_SKILL_NAME } from '../../core/mds-variants.js';
|
|
13
15
|
import { prefixSkillName } from '../../core/plugins.js';
|
|
14
16
|
/**
|
|
@@ -49,6 +51,39 @@ export function formatOverlaySummary(report, skillName = SKILL_REFS_SKILL_NAME)
|
|
|
49
51
|
}
|
|
50
52
|
return lines;
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Turn the replaced-link half of an InstallReport into summary lines, one per link.
|
|
56
|
+
*
|
|
57
|
+
* An install removes a link it finds where it keeps one of its own files or folders and writes a
|
|
58
|
+
* real file or folder there. What the link pointed to is not touched, which looks the same from the
|
|
59
|
+
* filesystem as an install that never met the link, so the line says both.
|
|
60
|
+
*
|
|
61
|
+
* Pure function — returns lines, logs nothing.
|
|
62
|
+
*/
|
|
63
|
+
export function formatReplacedLinksSummary(report) {
|
|
64
|
+
return report.replacedLinks.map(linkPath => ({ level: 'warn', message: replacedLinkMessage(linkPath) }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Turn the refused-folder half of an InstallReport into summary lines, one per folder.
|
|
68
|
+
*
|
|
69
|
+
* An install writes nothing at a path that is a link it could not remove, or not a folder where it
|
|
70
|
+
* needs one, and leaves the entry as it was. That looks the same on disk as an install that refreshed
|
|
71
|
+
* the path, so the line says what was skipped and what to do about it.
|
|
72
|
+
*
|
|
73
|
+
* Pure function — returns lines, logs nothing.
|
|
74
|
+
*/
|
|
75
|
+
export function formatRefusedFoldersSummary(report) {
|
|
76
|
+
return report.refusedFolders.map(folder => ({ level: 'warn', message: refusedFolderMessage(folder) }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Both halves in one list, the replaced links first: for a command that prints the links one
|
|
80
|
+
* converge met and has no other summary of its own.
|
|
81
|
+
*
|
|
82
|
+
* Pure function — returns lines, logs nothing.
|
|
83
|
+
*/
|
|
84
|
+
export function formatLinksSummary(report) {
|
|
85
|
+
return [...formatReplacedLinksSummary(report), ...formatRefusedFoldersSummary(report)];
|
|
86
|
+
}
|
|
52
87
|
/**
|
|
53
88
|
* The half of an overlay warning that describes what is actually on disk.
|
|
54
89
|
*
|
|
@@ -197,4 +232,98 @@ export function formatSkillScopeSummary(report, pluginListUnchanged) {
|
|
|
197
232
|
}
|
|
198
233
|
return lines;
|
|
199
234
|
}
|
|
235
|
+
const BACKUP_SHOWN_UNITS = 5;
|
|
236
|
+
/** One moved unit as `original -> backup`; a folder carries a trailing slash. */
|
|
237
|
+
function movedUnitLabel(unit) {
|
|
238
|
+
const slash = unit.kind === 'directory' ? '/' : '';
|
|
239
|
+
return `${unit.original}${slash} -> ${unit.backup}${slash}`;
|
|
240
|
+
}
|
|
241
|
+
function failureLine(failure) {
|
|
242
|
+
const where = escapeName(failure.path);
|
|
243
|
+
const reason = escapeName(failure.reason);
|
|
244
|
+
switch (failure.action) {
|
|
245
|
+
case 'move':
|
|
246
|
+
return { level: 'warn', message: `Could not move ${where} to the backup folder (${reason}) — it stays where it is` };
|
|
247
|
+
case 'scan':
|
|
248
|
+
return { level: 'warn', message: `Could not scan ${where} (${reason}) — nothing in it was moved or removed` };
|
|
249
|
+
case 'remove':
|
|
250
|
+
return { level: 'warn', message: `Could not remove ${where} (${reason}) — it stays where it is` };
|
|
251
|
+
default: {
|
|
252
|
+
const _exhaustive = failure.action;
|
|
253
|
+
void _exhaustive;
|
|
254
|
+
return { level: 'warn', message: `Could not finish a step on ${where} (${reason})` };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The lines for the bare legacy skill folders that init kept because it could not tell if devflow
|
|
260
|
+
* made them (`sweepLegacySkillFolders`): one line per folder for the first five, then one line that
|
|
261
|
+
* counts the rest. A run that kept none gives no lines. Every printed path goes through `escapeName`.
|
|
262
|
+
*/
|
|
263
|
+
export function formatKeptLegacySkills(kept) {
|
|
264
|
+
const lines = kept.slice(0, BACKUP_SHOWN_UNITS).map((folder) => ({
|
|
265
|
+
level: 'info',
|
|
266
|
+
message: `Kept ${escapeName(folder)}: devflow could not tell if it made this folder`,
|
|
267
|
+
}));
|
|
268
|
+
if (kept.length > BACKUP_SHOWN_UNITS) {
|
|
269
|
+
lines.push({ level: 'info', message: `Kept ${kept.length - BACKUP_SHOWN_UNITS} more folder(s) for the same reason` });
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Turn the backup session's record into summary lines: what moved aside, what stayed because it
|
|
275
|
+
* could not move, and which retired files were removed. An empty snapshot gives no lines, so a
|
|
276
|
+
* clean run prints nothing.
|
|
277
|
+
*
|
|
278
|
+
* A move is not a removal. The moved line has its own wording and counts only moved units (a moved
|
|
279
|
+
* folder is one), and it never feeds the "Removed N orphaned asset(s)" line of `formatSweepSummary`.
|
|
280
|
+
* One removed name prints on one line only, by the kind of removal:
|
|
281
|
+
*
|
|
282
|
+
* - the retired line below lists the retired files the session removed itself, with
|
|
283
|
+
* `BackupSession.removeListed` (the earlier releases' commands, agents, rules and skill files)
|
|
284
|
+
* or `BackupSession.removeExact` (the legacy sweep's command and rule names, and the legacy
|
|
285
|
+
* hook scripts);
|
|
286
|
+
* - `formatSweepSummary` lists the names the registry-diff sweeps removed, which the install
|
|
287
|
+
* report records in `sweptOrphans`. A sweep removes a name only after the session has not,
|
|
288
|
+
* and the session removes no name that a sweep records.
|
|
289
|
+
*
|
|
290
|
+
* Every printed name goes through `escapeName`. Pure function — returns lines, logs nothing.
|
|
291
|
+
*/
|
|
292
|
+
export function formatBackupSummary(snapshot) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
if (snapshot.moved.length > 0) {
|
|
295
|
+
lines.push({
|
|
296
|
+
level: 'info',
|
|
297
|
+
message: `Moved ${snapshot.moved.length} item(s) that devflow does not ship out of its folders: ` +
|
|
298
|
+
formatCappedList(snapshot.moved.map(movedUnitLabel), BACKUP_SHOWN_UNITS),
|
|
299
|
+
});
|
|
300
|
+
if (snapshot.runFolder !== null) {
|
|
301
|
+
lines.push({ level: 'info', message: `Backup folder: ${escapeName(snapshot.runFolder)}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (snapshot.retiredRemoved.length > 0) {
|
|
305
|
+
lines.push({
|
|
306
|
+
level: 'info',
|
|
307
|
+
message: `Removed ${snapshot.retiredRemoved.length} retired devflow file(s): ` +
|
|
308
|
+
formatCappedList(snapshot.retiredRemoved, BACKUP_SHOWN_UNITS),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const failure of snapshot.failures)
|
|
312
|
+
lines.push(failureLine(failure));
|
|
313
|
+
for (const hit of snapshot.capHits) {
|
|
314
|
+
lines.push({
|
|
315
|
+
level: 'warn',
|
|
316
|
+
message: `${escapeName(hit.folder)} holds ${hit.entries} entries, more than the limit of ${hit.cap} — ` +
|
|
317
|
+
'devflow left it whole and moved nothing out of it',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (snapshot.rootRefusal !== null) {
|
|
321
|
+
lines.push({
|
|
322
|
+
level: 'warn',
|
|
323
|
+
message: `Could not use the backup folder ${escapeName(snapshot.rootRefusal.path)} ` +
|
|
324
|
+
`(${escapeName(snapshot.rootRefusal.reason)}) — nothing was moved aside in this run`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
200
329
|
//# sourceMappingURL=install-report.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES } from '../../core/plugins.js';
|
|
5
|
+
import { RETIRED_SHIPPED } from '../../core/retired-shipped-names.js';
|
|
6
|
+
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
7
|
+
import { BARE_LEGACY_SKILL_HASHES } from '../../targets/claude-code/legacy-skill-hashes.js';
|
|
8
|
+
import { directoryState, isUnrealFolder } from '../../targets/claude-code/linked-folder.js';
|
|
9
|
+
/** The prefix of a skill folder in devflow's own namespace. A folder without it is bare. */
|
|
10
|
+
const NAMESPACE_PREFIX = 'devflow:';
|
|
11
|
+
/**
|
|
12
|
+
* Whether the sweep may remove files from `dir`: the install did not refuse it, nothing in it stays
|
|
13
|
+
* because a move failed or the folder was too large to scan, and `lstat` shows a real folder or
|
|
14
|
+
* nothing. `readdir` and `rm` of a path below a link act on the folder it points to.
|
|
15
|
+
*/
|
|
16
|
+
async function mayRemoveFrom(report, backups, dir) {
|
|
17
|
+
return !report.refusedFolders.includes(dir) && !backups.isKept(dir) && !(await isUnrealFolder(dir));
|
|
18
|
+
}
|
|
19
|
+
/** Each name once, in the order given. */
|
|
20
|
+
const uniqueNames = (...lists) => [...new Set(lists.flat())];
|
|
21
|
+
/**
|
|
22
|
+
* Remove `<folder>/<name>.md` for each name, by exact name. A name that is shipped today is never
|
|
23
|
+
* removed, and a file that stays is left for the session's record.
|
|
24
|
+
*/
|
|
25
|
+
async function removeRetiredMarkdown(folder, names, backups) {
|
|
26
|
+
const removed = [];
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
const file = path.join(folder, `${name}.md`);
|
|
29
|
+
if (backups.classifyAt(file, 'file') === 'shipped')
|
|
30
|
+
continue;
|
|
31
|
+
if ((await backups.removeExact(file)).status === 'removed')
|
|
32
|
+
removed.push(file);
|
|
33
|
+
}
|
|
34
|
+
return removed;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove the files that earlier releases installed into `commands/devflow` and `rules/devflow` and
|
|
38
|
+
* that no release installs now: the retired command and rule names and the legacy lists, each by its
|
|
39
|
+
* exact name. Runs on every install shape, since the full install's pre-clean covers only a full
|
|
40
|
+
* install. A file whose name only looks like a retired one (a `*-teams.md` of the user's) is not
|
|
41
|
+
* matched here: the install's pre-pass moves it aside and names it.
|
|
42
|
+
*
|
|
43
|
+
* A folder the install refused (`report.refusedFolders`), that the session could not scan, or that
|
|
44
|
+
* is a link now is skipped whole: the sweep reads and removes nothing there, and the folder a link
|
|
45
|
+
* points to is left as it is. Every removal goes through `backups`, so each removed file is named
|
|
46
|
+
* once, on the retired line of its record. Never throws.
|
|
47
|
+
*
|
|
48
|
+
* @param opts.legacyCommands - Legacy command names; added to the retired commands.
|
|
49
|
+
* @param opts.legacyRules - Legacy rule names; added to the retired rules.
|
|
50
|
+
*/
|
|
51
|
+
export async function sweepLegacyDevflowFiles(opts) {
|
|
52
|
+
const { claudeDir, report, backups } = opts;
|
|
53
|
+
const commands = uniqueNames(RETIRED_SHIPPED.commands, opts.legacyCommands ?? LEGACY_COMMAND_NAMES);
|
|
54
|
+
const rules = uniqueNames(RETIRED_SHIPPED.rules, opts.legacyRules ?? LEGACY_RULE_NAMES);
|
|
55
|
+
const removed = [];
|
|
56
|
+
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
57
|
+
if (await mayRemoveFrom(report, backups, commandsDir)) {
|
|
58
|
+
removed.push(...await removeRetiredMarkdown(commandsDir, commands, backups));
|
|
59
|
+
}
|
|
60
|
+
const rulesDir = path.join(claudeDir, 'rules', 'devflow');
|
|
61
|
+
if (await mayRemoveFrom(report, backups, rulesDir)) {
|
|
62
|
+
removed.push(...await removeRetiredMarkdown(rulesDir, rules, backups));
|
|
63
|
+
}
|
|
64
|
+
return { removed };
|
|
65
|
+
}
|
|
66
|
+
/** The most bytes of a `SKILL.md` the check reads. Every released copy is far smaller, so a larger file is not one. */
|
|
67
|
+
const MAX_SKILL_FILE_BYTES = 1024 * 1024;
|
|
68
|
+
/**
|
|
69
|
+
* Who made a bare legacy skill folder, judged from its `SKILL.md` alone:
|
|
70
|
+
* - `devflow`: the file is a regular file whose SHA-256 is one of `released`;
|
|
71
|
+
* - `other`: the file reads and is not one of them, or is larger than any release copy;
|
|
72
|
+
* - `unknown`: the check cannot decide. There is no `SKILL.md`, it is not a regular file, the
|
|
73
|
+
* folder or the file is a link, or a read fails.
|
|
74
|
+
* An `absent` folder needs no answer. Reads one file at most. Never throws.
|
|
75
|
+
*/
|
|
76
|
+
async function judgeBareSkillFolder(folder, released) {
|
|
77
|
+
const state = await directoryState(folder);
|
|
78
|
+
if (state === 'absent')
|
|
79
|
+
return 'absent';
|
|
80
|
+
if (state !== 'real')
|
|
81
|
+
return 'unknown';
|
|
82
|
+
try {
|
|
83
|
+
const skillFile = path.join(folder, 'SKILL.md');
|
|
84
|
+
const stat = await fs.lstat(skillFile);
|
|
85
|
+
if (!stat.isFile())
|
|
86
|
+
return 'unknown';
|
|
87
|
+
if (stat.size > MAX_SKILL_FILE_BYTES)
|
|
88
|
+
return 'other';
|
|
89
|
+
const digest = createHash('sha256').update(await fs.readFile(skillFile)).digest('hex');
|
|
90
|
+
return released.includes(digest) ? 'devflow' : 'other';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* {@link judgeBareSkillFolder} for the bare legacy skill `name`, against the released copies of that
|
|
98
|
+
* name. A name with no entry in `released` was never installed bare by a release, so a folder of
|
|
99
|
+
* that name is `other`. The one check that `devflow init` and `devflow uninstall` share.
|
|
100
|
+
*
|
|
101
|
+
* @param folder - The folder of the skill, `<skills folder>/<name>`.
|
|
102
|
+
* @param released - SHA-256 hashes of the released `SKILL.md` copies by bare name; defaults to the frozen table.
|
|
103
|
+
*/
|
|
104
|
+
export function judgeBareLegacySkill(folder, name, released = BARE_LEGACY_SKILL_HASHES) {
|
|
105
|
+
return judgeBareSkillFolder(folder, Object.hasOwn(released, name) ? released[name] : []);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Deal with the skill folders that earlier installs left under the legacy names: every name of the
|
|
109
|
+
* legacy skill list, bare and `devflow:`-prefixed.
|
|
110
|
+
*
|
|
111
|
+
* A `devflow:`-prefixed folder that no release listed moves whole, with one rename and no walk, and
|
|
112
|
+
* is named; it is never deleted. A bare folder lives in the skills folder that Claude Code and other
|
|
113
|
+
* tools share, so its name proves nothing (`python`, `plan` and `review` are names of a user's own
|
|
114
|
+
* skills too). It moves only when its `SKILL.md` is a copy that a release of devflow installed
|
|
115
|
+
* (`released`). A bare folder whose `SKILL.md` is any other file stays where it is, and nothing is
|
|
116
|
+
* said. A bare folder that the check cannot judge stays, is never deleted, and is returned in
|
|
117
|
+
* `kept`, so the caller names it. A name with a listing in `retiredFiles` (the files a release
|
|
118
|
+
* shipped in that folder) goes by the listing instead: the listed files are removed, the rest of
|
|
119
|
+
* the folder moves, and a plain `rmdir` removes the folder only when it is empty. A move that fails
|
|
120
|
+
* leaves the folder and its files where they are, and the session records it. A name that is shipped
|
|
121
|
+
* today, or that an earlier step could not move, is skipped, and so is every name when the skills
|
|
122
|
+
* folder is a link or not a folder: nothing is moved through a link. Nothing here removes a folder
|
|
123
|
+
* with recursion. Never throws.
|
|
124
|
+
*
|
|
125
|
+
* @param opts.legacySkills - Folder names to deal with; defaults to the legacy skill list.
|
|
126
|
+
* @param opts.retiredFiles - Released file listings by folder name; defaults to the retired registry.
|
|
127
|
+
* @param opts.released - SHA-256 hashes of the released `SKILL.md` copies by bare name; defaults to the frozen table.
|
|
128
|
+
*/
|
|
129
|
+
export async function sweepLegacySkillFolders(opts) {
|
|
130
|
+
const { backups } = opts;
|
|
131
|
+
const retiredFiles = opts.retiredFiles ?? RETIRED_SHIPPED.skillFiles;
|
|
132
|
+
const released = opts.released ?? BARE_LEGACY_SKILL_HASHES;
|
|
133
|
+
const kept = [];
|
|
134
|
+
const skillsDir = path.join(opts.claudeDir, 'skills');
|
|
135
|
+
if ((await directoryState(skillsDir)) !== 'real')
|
|
136
|
+
return { kept };
|
|
137
|
+
for (const name of opts.legacySkills ?? LEGACY_SKILL_NAMES) {
|
|
138
|
+
const folder = path.join(skillsDir, name);
|
|
139
|
+
if (backups.isKept(folder) || backups.classifyAt(folder, 'directory') === 'shipped')
|
|
140
|
+
continue;
|
|
141
|
+
const listed = Object.hasOwn(retiredFiles, name) && retiredFiles[name].length > 0;
|
|
142
|
+
if (listed && (await directoryState(folder)) === 'real') {
|
|
143
|
+
await backups.prepass([folder]);
|
|
144
|
+
await backups.removeListed(folder);
|
|
145
|
+
}
|
|
146
|
+
else if (name.startsWith(NAMESPACE_PREFIX)) {
|
|
147
|
+
await backups.moveWhole(folder);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const verdict = await judgeBareLegacySkill(folder, name, released);
|
|
151
|
+
if (verdict === 'devflow')
|
|
152
|
+
await backups.moveWhole(folder);
|
|
153
|
+
else if (verdict === 'unknown')
|
|
154
|
+
kept.push(folder);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { kept };
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=legacy-sweep.js.map
|
|
@@ -31,6 +31,7 @@ import { stripAnsi } from '../../hud/colors.js';
|
|
|
31
31
|
import { reapplyAgentMapping, revertExternalAgents, countExternalMappedAgents, readAgentMapping, } from '../../core/agent-models.js';
|
|
32
32
|
import { getExternalModelsCached, discoverExternalModels, } from '../../core/model-discovery.js';
|
|
33
33
|
import { modelCacheDir } from '../../core/cache.js';
|
|
34
|
+
import { formatLinksSummary } from './install-report.js';
|
|
34
35
|
import { getClaudeDirectory, getDevFlowDirectory, getHomeDirectory, } from '../../targets/claude-code/claude-paths.js';
|
|
35
36
|
import { devflowHookOwner, ensureHook, hasHook, removeHooks, runHookCommand, } from '../../targets/claude-code/hooks.js';
|
|
36
37
|
function Ok(value) {
|
|
@@ -1389,6 +1390,8 @@ async function runEnable(portOption) {
|
|
|
1389
1390
|
s.message('Warming model cache...');
|
|
1390
1391
|
await discoverExternalModels(cacheDir, logPath).catch(() => { });
|
|
1391
1392
|
s.stop(color.green('External model routing enabled'));
|
|
1393
|
+
for (const { message } of formatLinksSummary(reapplyResult))
|
|
1394
|
+
p.log.warn(message);
|
|
1392
1395
|
// D-P4-1: applies-on-restart — env var takes effect only for new sessions
|
|
1393
1396
|
p.log.info(color.dim('Context-window enforcement disabled for relay-routed models — applies to new Claude Code sessions'));
|
|
1394
1397
|
if (adopted) {
|
|
@@ -1472,7 +1475,9 @@ async function runDisable() {
|
|
|
1472
1475
|
// Step 3: Sync manifest
|
|
1473
1476
|
await syncManifestFeature(devflowDir, 'proxy', false);
|
|
1474
1477
|
// Step 4: Revert external agents to shipped defaults
|
|
1475
|
-
await revertExternalAgents({ installDir, devflowDir });
|
|
1478
|
+
const revertResult = await revertExternalAgents({ installDir, devflowDir });
|
|
1479
|
+
for (const { message } of formatLinksSummary(revertResult))
|
|
1480
|
+
p.log.warn(message);
|
|
1476
1481
|
p.log.success('External model routing disabled');
|
|
1477
1482
|
// Step 5: Terminate the relay process.
|
|
1478
1483
|
// Identity is confirmed via TCP + health before signalling — a recycled PID
|
|
@@ -10,6 +10,10 @@ import { rulesDir } from '../../core/assets.js';
|
|
|
10
10
|
import { installAllRules, validateRuleShadow } from '../../targets/claude-code/installer.js';
|
|
11
11
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
12
12
|
import { convergeBehaviorFromManifest } from '../../targets/claude-code/behavior-install.js';
|
|
13
|
+
import { newLinkReport, refuseUnrealFolder, refusedFolderMessage } from '../../targets/claude-code/linked-folder.js';
|
|
14
|
+
import { removeRulesFolder, rulesFolderRefusedMessage } from '../../targets/claude-code/rules-folder.js';
|
|
15
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
16
|
+
import { formatLinksSummary } from './install-report.js';
|
|
13
17
|
/**
|
|
14
18
|
* Check whether a rule has a user shadow file at ~/.devflow/rules/{name}.md.
|
|
15
19
|
*/
|
|
@@ -203,27 +207,43 @@ async function handleRuleUnshadow(name, allRules, devflowDir) {
|
|
|
203
207
|
*
|
|
204
208
|
* Warn-not-throw on per-item compliance convergence failure, so a convergence
|
|
205
209
|
* failure never undoes the rules install.
|
|
210
|
+
*
|
|
211
|
+
* The record of `backups` prints once, as the enable returns or throws.
|
|
206
212
|
*/
|
|
207
|
-
export
|
|
213
|
+
export function runRulesEnable(claudeDir, devflowDir, backups) {
|
|
214
|
+
return runWithBackupReport(backups, printBackups => enableRules(claudeDir, devflowDir, backups, printBackups));
|
|
215
|
+
}
|
|
216
|
+
async function enableRules(claudeDir, devflowDir, backups, printBackups) {
|
|
208
217
|
const rulesTarget = path.join(claudeDir, 'rules', 'devflow');
|
|
209
218
|
const manifest = await readManifest(devflowDir);
|
|
210
219
|
if (!manifest) {
|
|
211
220
|
p.log.error('No manifest found. Run devflow init first.');
|
|
221
|
+
printBackups();
|
|
212
222
|
process.exit(1);
|
|
213
223
|
}
|
|
214
224
|
const installedPlugins = DEVFLOW_PLUGINS.filter(pl => manifest.plugins.includes(pl.name));
|
|
215
225
|
const rulesMap = buildRulesMap(installedPlugins);
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
// Clear stale rules from previously uninstalled plugins before re-installing: the files devflow did
|
|
227
|
+
// not ship move to the backup folder, and the rest of the folder is removed by the session's list.
|
|
228
|
+
// A rules folder that is a link is unlinked and replaced by a real folder, never emptied through,
|
|
229
|
+
// and the link is reported. Something that is not a folder stays and is refused below.
|
|
230
|
+
const links = newLinkReport();
|
|
231
|
+
if ((await removeRulesFolder(rulesTarget, backups)) === 'unlinked')
|
|
232
|
+
links.replacedLinks.push(rulesTarget);
|
|
233
|
+
// A link that could not be removed is still there, and `mkdir` and every rule write would follow it.
|
|
234
|
+
// Prove the folder first; a refused folder gets no rule, no manifest write and one refusal line.
|
|
235
|
+
const rulesFolderRefused = await refuseUnrealFolder(links, rulesTarget);
|
|
219
236
|
let outcomes = [];
|
|
220
|
-
let installFailed =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
let installFailed = rulesFolderRefused;
|
|
238
|
+
if (!rulesFolderRefused) {
|
|
239
|
+
await fs.mkdir(rulesTarget, { recursive: true });
|
|
240
|
+
try {
|
|
241
|
+
outcomes = await installAllRules(rulesMap, devflowDir, rulesTarget, links);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
p.log.error(`Rules installation failed — rules directory has been cleared: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
+
installFailed = true;
|
|
246
|
+
}
|
|
227
247
|
}
|
|
228
248
|
if (!installFailed) {
|
|
229
249
|
const shadowedCount = outcomes.filter(o => o.outcome === 'shadow').length;
|
|
@@ -239,6 +259,14 @@ export async function runRulesEnable(claudeDir, devflowDir) {
|
|
|
239
259
|
await writeManifest(devflowDir, manifest);
|
|
240
260
|
p.log.success(`Installed ${rulesMap.size} rule(s) to ${color.dim(rulesTarget)}${shadowSuffix}`);
|
|
241
261
|
}
|
|
262
|
+
for (const { message } of formatLinksSummary(links))
|
|
263
|
+
p.log.warn(message);
|
|
264
|
+
// Each converge below proves the rules folder again and names a refused one itself; the line above
|
|
265
|
+
// has already named it, so the repeat is dropped.
|
|
266
|
+
const warnConverge = (msg) => {
|
|
267
|
+
if (!(rulesFolderRefused && msg === refusedFolderMessage(rulesTarget)))
|
|
268
|
+
p.log.warn(msg);
|
|
269
|
+
};
|
|
242
270
|
// Converge compliance artifacts unconditionally — convergeFromManifest handles the
|
|
243
271
|
// disabled case itself (removes stale artifacts). Running this regardless of installFailed
|
|
244
272
|
// prevents a rules-install failure from leaving the compliance rule in a stale state.
|
|
@@ -249,8 +277,9 @@ export async function runRulesEnable(claudeDir, devflowDir) {
|
|
|
249
277
|
claudeDir,
|
|
250
278
|
devflowDir,
|
|
251
279
|
manifest,
|
|
252
|
-
warn:
|
|
280
|
+
warn: warnConverge,
|
|
253
281
|
rulesEnabledOverride: !installFailed,
|
|
282
|
+
backups,
|
|
254
283
|
});
|
|
255
284
|
}
|
|
256
285
|
catch (err) {
|
|
@@ -265,7 +294,7 @@ export async function runRulesEnable(claudeDir, devflowDir) {
|
|
|
265
294
|
claudeDir,
|
|
266
295
|
devflowDir,
|
|
267
296
|
manifest,
|
|
268
|
-
warn:
|
|
297
|
+
warn: warnConverge,
|
|
269
298
|
rulesEnabledOverride: !installFailed,
|
|
270
299
|
});
|
|
271
300
|
}
|
|
@@ -308,17 +337,42 @@ export const rulesCommand = new Command('rules')
|
|
|
308
337
|
}
|
|
309
338
|
// No positional action — fall through to flag-based handling
|
|
310
339
|
if (options.enable) {
|
|
311
|
-
await
|
|
340
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
341
|
+
if (backups === null)
|
|
342
|
+
return;
|
|
343
|
+
await runRulesEnable(claudeDir, devflowDir, backups);
|
|
312
344
|
}
|
|
313
345
|
else if (options.disable) {
|
|
314
|
-
await
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await
|
|
346
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
347
|
+
if (backups === null)
|
|
348
|
+
return;
|
|
349
|
+
const removal = await runWithBackupReport(backups, async () => {
|
|
350
|
+
const outcome = await removeRulesFolder(rulesTarget, backups);
|
|
351
|
+
const manifest = await readManifest(devflowDir);
|
|
352
|
+
if (manifest) {
|
|
353
|
+
manifest.features.rules = false;
|
|
354
|
+
manifest.updatedAt = new Date().toISOString();
|
|
355
|
+
await writeManifest(devflowDir, manifest);
|
|
356
|
+
}
|
|
357
|
+
return outcome;
|
|
358
|
+
});
|
|
359
|
+
switch (removal) {
|
|
360
|
+
case 'kept':
|
|
361
|
+
p.log.warn(`Rules disabled — ${rulesTarget} stays, because it still holds files that devflow did not remove`);
|
|
362
|
+
break;
|
|
363
|
+
case 'refused':
|
|
364
|
+
p.log.warn(`Rules disabled. ${rulesFolderRefusedMessage(rulesTarget)}`);
|
|
365
|
+
break;
|
|
366
|
+
case 'absent':
|
|
367
|
+
case 'removed':
|
|
368
|
+
case 'unlinked':
|
|
369
|
+
p.log.success('Rules disabled — removed rules directory');
|
|
370
|
+
break;
|
|
371
|
+
default: {
|
|
372
|
+
const unreachable = removal;
|
|
373
|
+
void unreachable;
|
|
374
|
+
}
|
|
320
375
|
}
|
|
321
|
-
p.log.success('Rules disabled — removed rules directory');
|
|
322
376
|
}
|
|
323
377
|
else if (options.status) {
|
|
324
378
|
let installedFiles = [];
|
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
7
|
-
import { getAllSkillNames, prefixSkillName, unprefixSkillName, skillOwners, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
7
|
+
import { getAllSkillNames, prefixSkillName, unprefixSkillName, skillOwners, FEATURE_OWNED_SKILLS, WORKFLOW_FOLDER_NAMES } from '../../core/plugins.js';
|
|
8
8
|
import { skillsDir } from '../../core/assets.js';
|
|
9
9
|
import { copyDirectory, validateSkillShadow } from '../../targets/claude-code/installer.js';
|
|
10
10
|
/**
|
|
@@ -42,6 +42,22 @@ export function shadowSeedDir(bareName, installedDir) {
|
|
|
42
42
|
return sourceDir;
|
|
43
43
|
return installedDir ?? sourceDir;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* D-WORKFLOW-SHADOW-REFUSAL: why `devflow skills shadow <name>` refuses a workflow that ships as a
|
|
47
|
+
* skill folder, or null for any other name.
|
|
48
|
+
*
|
|
49
|
+
* A folder installs from the build tree of the machine's exact variant key, with no source
|
|
50
|
+
* directory to copy and no shadow to overlay, so a shadow of its name could never apply. Refusing
|
|
51
|
+
* at the command says so before the user edits a copy that does nothing; the install ignores a
|
|
52
|
+
* shadow that is already there, with a warning, and keeps it.
|
|
53
|
+
*
|
|
54
|
+
* @param folderWorkflows - The target-layer set; the default is the real set.
|
|
55
|
+
*/
|
|
56
|
+
export function workflowFolderShadowRefusal(bareName, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
57
|
+
if (!folderWorkflows.has(bareName))
|
|
58
|
+
return null;
|
|
59
|
+
return `${bareName} ships as a skill folder built for the machine's variant, so a shadow cannot replace it. Nothing was shadowed.`;
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* Check if a skill has a shadow (personal override).
|
|
47
63
|
*/
|
|
@@ -85,16 +101,19 @@ function buildSkillShadowTag(shadowState) {
|
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Run one `devflow skills` action: shadow, unshadow or list.
|
|
106
|
+
*
|
|
107
|
+
* @param folderWorkflows - The workflows that ship as skill folders; the default is the real
|
|
108
|
+
* set. They count as known skills, `shadow` refuses them, and `list` tags a leftover shadow
|
|
109
|
+
* of one as ignored. Tests pass a fixture set, because the real set is empty.
|
|
110
|
+
*/
|
|
111
|
+
export async function runSkillsAction(action, name, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
93
112
|
const devflowDir = getDevFlowDirectory();
|
|
94
113
|
const claudeDir = getClaudeDirectory();
|
|
95
114
|
// Union FEATURE_OWNED_SKILLS (compliance) so shadow/unshadow/list cover feature-managed
|
|
96
115
|
// skills that left the plugin registry but still live in src/assets/skills/ (step 1.5).
|
|
97
|
-
const allSkills = [...getAllSkillNames(), ...FEATURE_OWNED_SKILLS];
|
|
116
|
+
const allSkills = [...getAllSkillNames(folderWorkflows), ...FEATURE_OWNED_SKILLS];
|
|
98
117
|
if (action === 'shadow') {
|
|
99
118
|
if (!name) {
|
|
100
119
|
p.log.error('Skill name required. Usage: devflow skills shadow <name>');
|
|
@@ -103,6 +122,11 @@ export const skillsCommand = new Command('skills')
|
|
|
103
122
|
}
|
|
104
123
|
// Accept both bare and prefixed input
|
|
105
124
|
const bareName = unprefixSkillName(name);
|
|
125
|
+
const refusal = workflowFolderShadowRefusal(bareName, folderWorkflows);
|
|
126
|
+
if (refusal !== null) {
|
|
127
|
+
p.log.error(refusal);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
106
130
|
if (!allSkills.includes(bareName)) {
|
|
107
131
|
p.log.error(`Unknown skill: ${bareName}`);
|
|
108
132
|
p.log.info(`Available skills: ${allSkills.join(', ')}`);
|
|
@@ -183,7 +207,11 @@ export const skillsCommand = new Command('skills')
|
|
|
183
207
|
const provenance = installed
|
|
184
208
|
? color.dim(`installed because: ${describeOwners(skill, ownersBySkill.get(skill))}`)
|
|
185
209
|
: color.dim(`not installed — provided by: ${describeOwners(skill, ownersBySkill.get(skill))}`);
|
|
186
|
-
|
|
210
|
+
// A shadow of a skill folder is kept but never applied (D-WORKFLOW-SHADOW-REFUSAL).
|
|
211
|
+
const tag = workflowFolderShadowRefusal(skill, folderWorkflows) !== null && shadowState !== 'none'
|
|
212
|
+
? color.yellow('shadow ignored — skill folder')
|
|
213
|
+
: buildSkillShadowTag(shadowState);
|
|
214
|
+
return ` ${color.cyan(skill.padEnd(28))} ${tag.padEnd(20)} ${provenance}`;
|
|
187
215
|
});
|
|
188
216
|
// Orphan shadows: in ~/.devflow/skills/ but not a known skill
|
|
189
217
|
for (const dirName of shadowDirNames) {
|
|
@@ -200,5 +228,10 @@ export const skillsCommand = new Command('skills')
|
|
|
200
228
|
p.log.info('Usage: devflow skills <shadow|unshadow|list> [name]');
|
|
201
229
|
process.exit(1);
|
|
202
230
|
}
|
|
203
|
-
}
|
|
231
|
+
}
|
|
232
|
+
export const skillsCommand = new Command('skills')
|
|
233
|
+
.description('Manage skill overrides (shadow/unshadow/list)')
|
|
234
|
+
.argument('<action>', 'Action: shadow, unshadow, or list')
|
|
235
|
+
.argument('[name]', 'Skill name (required for shadow/unshadow)')
|
|
236
|
+
.action((action, name) => runSkillsAction(action, name));
|
|
204
237
|
//# sourceMappingURL=skills.js.map
|
|
@@ -56,12 +56,18 @@ export async function readTrackerProvenance(devflowDir, provider) {
|
|
|
56
56
|
* every install carries them all (D-INSTALL-ALL-PROVIDERS) — rather than listing
|
|
57
57
|
* the directory: the question a user asks `--status` is "can the agent load what
|
|
58
58
|
* it is told to load?", and a stray file in the tree is not an answer to it.
|
|
59
|
+
*
|
|
60
|
+
* `compliance` is the machine's compliance switch, read from the install manifest by the
|
|
61
|
+
* caller and never from a repository file: a compliance-off machine installs the compliance-off
|
|
62
|
+
* manifest, which holds fewer references, so counting against the compliance-on one would read
|
|
63
|
+
* every such machine as damaged.
|
|
64
|
+
*
|
|
59
65
|
* Never throws.
|
|
60
66
|
*/
|
|
61
|
-
export async function readTrackerMechanics(claudeDir) {
|
|
67
|
+
export async function readTrackerMechanics(claudeDir, compliance) {
|
|
62
68
|
const root = path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references');
|
|
63
69
|
let present = 0;
|
|
64
|
-
for (const rel of installedReferenceManifest()) {
|
|
70
|
+
for (const rel of installedReferenceManifest({ compliance })) {
|
|
65
71
|
try {
|
|
66
72
|
await fs.access(path.join(root, ...rel.split('/')));
|
|
67
73
|
present++;
|
|
@@ -292,7 +298,7 @@ export const trackerCommand = new Command('tracker')
|
|
|
292
298
|
const selection = repoTrackerSelection(settingsModule, { dir: process.cwd() });
|
|
293
299
|
const effective = selection?.provider ?? current.provider;
|
|
294
300
|
const conventions = await readTrackerStatusConventions(devflowDir, effective);
|
|
295
|
-
const mechanics = await readTrackerMechanics(getClaudeDirectory());
|
|
301
|
+
const mechanics = await readTrackerMechanics(getClaudeDirectory(), manifest.features.compliance.enabled);
|
|
296
302
|
// [D-F] Inspecting the status re-arms the attempt counters. --status is
|
|
297
303
|
// the command a capped user reaches for to find out why nothing is being
|
|
298
304
|
// learned, so it is the command that has to hand back another five
|