devflow-kit 2.4.0 → 3.0.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 +229 -0
- package/README.md +111 -18
- package/dist/agents/git.md +822 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +48 -55
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +61 -27
- package/dist/cli/commands/init.js +649 -240
- package/dist/cli/commands/install-report.js +200 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +35 -37
- package/dist/cli/commands/learning.js +79 -57
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +134 -135
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +81 -29
- package/dist/cli/commands/skills.js +71 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +277 -0
- package/dist/cli/commands/uninstall.js +520 -169
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +58 -14
- package/dist/commands/code-review.md +110 -32
- package/dist/commands/debug.md +55 -11
- package/dist/commands/dynamic-build.md +344 -73
- package/dist/commands/dynamic-plan.md +77 -27
- package/dist/commands/dynamic-profile.md +25 -11
- package/dist/commands/dynamic-tickets.md +76 -15
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +314 -62
- package/dist/commands/plan.md +146 -32
- package/dist/commands/release.md +64 -17
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +196 -68
- package/dist/commands/self-review.md +45 -9
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +363 -0
- package/dist/core/feature-config.js +200 -65
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +34 -6
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +38 -10
- package/dist/core/mds-variants.js +798 -0
- package/dist/core/migrations.js +49 -23
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +361 -12
- package/dist/core/project-paths.js +1 -18
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +494 -0
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +1187 -32
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +366 -151
- package/dist/targets/claude-code/tracker-install.js +134 -0
- package/package.json +8 -6
- package/src/assets/agents/code.md +45 -6
- package/src/assets/agents/design.md +2 -1
- package/src/assets/agents/git.mds +825 -0
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +474 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +13 -11
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +2 -2
- package/src/assets/commands/_partials/_publication.mds +8 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +31 -19
- package/src/assets/commands/code-review.mds +67 -41
- package/src/assets/commands/debug.mds +13 -7
- package/src/assets/commands/dynamic-build.mds +274 -66
- package/src/assets/commands/dynamic-plan.mds +50 -23
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +63 -16
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +234 -67
- package/src/assets/commands/plan.mds +91 -33
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +150 -78
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +305 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +40 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -8
- package/src/assets/scripts/hooks/capture-question +18 -8
- package/src/assets/scripts/hooks/capture-turn +27 -13
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +228 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +22 -13
- package/src/assets/scripts/hooks/pre-compact-memory +44 -15
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +146 -28
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +534 -20
- package/src/assets/scripts/hooks/session-start-memory +38 -15
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +4 -2
- package/src/assets/skills/docs-framework/SKILL.md +11 -10
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
- package/src/assets/agents/git.md +0 -938
|
@@ -4,8 +4,8 @@ import * as p from '@clack/prompts';
|
|
|
4
4
|
import color from 'picocolors';
|
|
5
5
|
import { getClaudeDirectory, getDevFlowDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
|
|
6
6
|
import { readManifest, syncManifestFeature } from '../../core/manifest.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { applyUserSecurityDenyList, stripUserDenyList, detectDenyState, DEVFLOW_HISTORICAL_DENY, removeManagedSettings, installManagedSettings, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
|
|
8
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
9
9
|
import { promises as fs } from 'fs';
|
|
10
10
|
import { getPackageRoot } from '../../core/paths.js';
|
|
11
11
|
// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
@@ -23,7 +23,74 @@ export function countDenyEntries(settingsJson) {
|
|
|
23
23
|
return 0;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Remove the Devflow deny list from the managed settings file — the ONE
|
|
28
|
+
* implementation `devflow security --disable` and `devflow init --security none`
|
|
29
|
+
* share, so "none" means none in every location (#378).
|
|
30
|
+
*
|
|
31
|
+
* Gated on detectDenyState's parse-safe signal rather than on the file merely
|
|
32
|
+
* existing: removeManagedSettings parses the file without a guard, so a corrupt
|
|
33
|
+
* managed file must be classified here and never reach it (SF5). Never throws —
|
|
34
|
+
* a permission failure is the `failed` outcome, reported by the caller.
|
|
35
|
+
*
|
|
36
|
+
* `managedPath` is the injectable seam for tests (see removeManagedSettings);
|
|
37
|
+
* production callers omit it and get the platform path.
|
|
38
|
+
*/
|
|
39
|
+
export async function removeManagedDenyList(rootDir, verbose, managedPath) {
|
|
40
|
+
let target;
|
|
41
|
+
if (managedPath !== undefined) {
|
|
42
|
+
target = managedPath;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
try {
|
|
46
|
+
target = getManagedSettingsPath();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { kind: 'absent' };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let content;
|
|
53
|
+
try {
|
|
54
|
+
content = await fs.readFile(target, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { kind: 'absent' };
|
|
58
|
+
}
|
|
59
|
+
if (!detectDenyState(null, true, content).managed) {
|
|
60
|
+
return { kind: 'no-devflow-entries', path: target };
|
|
61
|
+
}
|
|
62
|
+
const removed = await removeManagedSettings(rootDir, verbose, target);
|
|
63
|
+
return removed ? { kind: 'removed', path: target } : { kind: 'failed', path: target };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The user-facing line for a ManagedDenyRemoval. Pure.
|
|
67
|
+
*
|
|
68
|
+
* D-MANAGED-REMOVAL-REMEDY: both `devflow security --disable` and
|
|
69
|
+
* `devflow init --security none` print this line, so the `failed` remedy names
|
|
70
|
+
* no command — pointing at `devflow security --disable` sent a user of that very
|
|
71
|
+
* command back to it. A `failed` removal means the write needed admin rights and
|
|
72
|
+
* the sudo fallback could not run (no interactive terminal) or was declined, so
|
|
73
|
+
* the remedy is exactly that: re-run interactively and accept the prompt, or
|
|
74
|
+
* edit the named file as an administrator.
|
|
75
|
+
*/
|
|
76
|
+
export function describeManagedDenyRemoval(outcome) {
|
|
77
|
+
switch (outcome.kind) {
|
|
78
|
+
case 'removed':
|
|
79
|
+
return { level: 'info', text: `Security deny list removed from managed settings (${outcome.path})` };
|
|
80
|
+
case 'absent':
|
|
81
|
+
return { level: 'info', text: 'No managed settings to remove' };
|
|
82
|
+
case 'no-devflow-entries':
|
|
83
|
+
return { level: 'warn', text: 'Managed settings file exists but contains no Devflow deny entries — skipping' };
|
|
84
|
+
case 'failed':
|
|
85
|
+
return {
|
|
86
|
+
level: 'warn',
|
|
87
|
+
text: `Could not remove the Devflow deny list from managed settings (${outcome.path}) — ` +
|
|
88
|
+
'it needs admin rights, and sudo was declined or unavailable. Re-run this command in an ' +
|
|
89
|
+
'interactive terminal and accept the sudo prompt, or, as an administrator, remove the Devflow ' +
|
|
90
|
+
`entries from permissions.deny in ${outcome.path}.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
27
94
|
export const securityCommand = new Command('security')
|
|
28
95
|
.description('Manage the security deny list (permissions.deny in Claude Code settings)')
|
|
29
96
|
.option('--status', 'Show current deny list state and entry counts')
|
|
@@ -116,16 +183,8 @@ export const securityCommand = new Command('security')
|
|
|
116
183
|
}
|
|
117
184
|
}
|
|
118
185
|
else {
|
|
119
|
-
// User mode: merge into ~/.claude/settings.json
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
existing = await fs.readFile(userSettingsPath, 'utf-8');
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
existing = '{}';
|
|
126
|
-
}
|
|
127
|
-
const merged = mergeDenyList(existing, templateDeny);
|
|
128
|
-
await writeFileAtomicExclusive(userSettingsPath, merged);
|
|
186
|
+
// User mode: merge into ~/.claude/settings.json, converging retired entries
|
|
187
|
+
const merged = await applyUserSecurityDenyList(userSettingsPath, templateDeny);
|
|
129
188
|
const count = countDenyEntries(merged);
|
|
130
189
|
p.log.success(`Security deny list applied to user settings (${count} entries)`);
|
|
131
190
|
p.log.info(` Location: ${color.dim(userSettingsPath)}`);
|
|
@@ -147,7 +206,7 @@ export const securityCommand = new Command('security')
|
|
|
147
206
|
}
|
|
148
207
|
const { json: stripped, removed } = stripUserDenyList(userSettingsJson, DEVFLOW_HISTORICAL_DENY);
|
|
149
208
|
if (removed.length > 0) {
|
|
150
|
-
await
|
|
209
|
+
await writeSettingsFileAtomic(userSettingsPath, stripped);
|
|
151
210
|
p.log.success(`Removed ${removed.length} entries from user settings:`);
|
|
152
211
|
for (const entry of removed) {
|
|
153
212
|
p.log.info(` ${color.dim(entry)}`);
|
|
@@ -157,21 +216,14 @@ export const securityCommand = new Command('security')
|
|
|
157
216
|
p.log.info('No Devflow deny entries found in user settings');
|
|
158
217
|
}
|
|
159
218
|
}
|
|
160
|
-
// Managed settings:
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
else if (managedExists && !detected.managed) {
|
|
169
|
-
// File exists but is unparseable or contains no Devflow entries — warn and skip.
|
|
170
|
-
p.log.warn('Managed settings file exists but contains no Devflow deny entries — skipping');
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
p.log.info('No managed settings to remove');
|
|
174
|
-
}
|
|
219
|
+
// Managed settings: the shared removal (parse-safe gate, never throws — SF5;
|
|
220
|
+
// a throw here would skip the manifest sync below after user settings were
|
|
221
|
+
// already stripped).
|
|
222
|
+
const managedMsg = describeManagedDenyRemoval(await removeManagedDenyList(rootDir, false));
|
|
223
|
+
if (managedMsg.level === 'warn')
|
|
224
|
+
p.log.warn(managedMsg.text);
|
|
225
|
+
else
|
|
226
|
+
p.log.info(managedMsg.text);
|
|
175
227
|
await syncManifestFeature(devflowDir, 'security', 'none');
|
|
176
228
|
return;
|
|
177
229
|
}
|
|
@@ -4,7 +4,8 @@ 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, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
7
|
+
import { getAllSkillNames, prefixSkillName, unprefixSkillName, skillOwners, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
8
|
+
import { skillsDir } from '../../core/assets.js';
|
|
8
9
|
import { copyDirectory, validateSkillShadow } from '../../targets/claude-code/installer.js';
|
|
9
10
|
/**
|
|
10
11
|
* Check if a directory exists.
|
|
@@ -24,6 +25,23 @@ async function dirExists(dirPath) {
|
|
|
24
25
|
function getShadowDir(devflowDir, skillName) {
|
|
25
26
|
return path.join(devflowDir, 'skills', unprefixSkillName(skillName));
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The directory a new shadow of `bareName` is copied from.
|
|
30
|
+
*
|
|
31
|
+
* D-COMPLIANCE-SHADOW-SOURCE (mirrors seedRuleShadow's Tier-1 skip in rules.ts):
|
|
32
|
+
* a FEATURE_OWNED skill always seeds from the shipped SOURCE. Its installed copy
|
|
33
|
+
* is composed — every `${DEVFLOW_COMPLIANCE_*}` token already replaced with the
|
|
34
|
+
* frameworks of that moment — and a token-free shadow passes through composition
|
|
35
|
+
* byte-identical (C1), so seeding from it would freeze that stamp: a later
|
|
36
|
+
* `devflow compliance --set` could never re-stamp the skill. Every other skill
|
|
37
|
+
* seeds from its installed copy when there is one, else from source.
|
|
38
|
+
*/
|
|
39
|
+
export function shadowSeedDir(bareName, installedDir) {
|
|
40
|
+
const sourceDir = path.join(skillsDir(), bareName);
|
|
41
|
+
if (FEATURE_OWNED_SKILLS.includes(bareName))
|
|
42
|
+
return sourceDir;
|
|
43
|
+
return installedDir ?? sourceDir;
|
|
44
|
+
}
|
|
27
45
|
/**
|
|
28
46
|
* Check if a skill has a shadow (personal override).
|
|
29
47
|
*/
|
|
@@ -31,6 +49,26 @@ export async function hasShadow(skillName, devflowDir) {
|
|
|
31
49
|
const dir = devflowDir ?? getDevFlowDirectory();
|
|
32
50
|
return dirExists(getShadowDir(dir, skillName));
|
|
33
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Which plugin(s) a user must select to get a skill — rendered for a message.
|
|
54
|
+
*
|
|
55
|
+
* Every declarer, never the first: once install is scoped, the question this
|
|
56
|
+
* answers is "which plugin do I select to keep this?", and a first-wins answer
|
|
57
|
+
* names one plugin out of several that would each do (D-ALL-OWNERS).
|
|
58
|
+
* FEATURE_OWNED skills have no plugin owner at all and say so, because telling
|
|
59
|
+
* a user to select a plugin for `compliance` would send them looking for one
|
|
60
|
+
* that does not exist. Compliance is installed on every machine
|
|
61
|
+
* (D-COMPLIANCE-INSTALL-ALWAYS), so its feature is the whole answer.
|
|
62
|
+
*/
|
|
63
|
+
function describeOwners(bareName, owners) {
|
|
64
|
+
const declarers = owners ?? skillOwners(bareName);
|
|
65
|
+
if (declarers.length === 0) {
|
|
66
|
+
return FEATURE_OWNED_SKILLS.includes(bareName)
|
|
67
|
+
? 'the compliance feature (every install)'
|
|
68
|
+
: 'no plugin';
|
|
69
|
+
}
|
|
70
|
+
return declarers.join(' or ');
|
|
71
|
+
}
|
|
34
72
|
/** Render the shadow-state display tag for a skill. Exhaustive switch catches new states at compile time. */
|
|
35
73
|
function buildSkillShadowTag(shadowState) {
|
|
36
74
|
switch (shadowState) {
|
|
@@ -72,8 +110,16 @@ export const skillsCommand = new Command('skills')
|
|
|
72
110
|
}
|
|
73
111
|
const prefixedName = prefixSkillName(bareName);
|
|
74
112
|
const installedSkillDir = path.join(claudeDir, 'skills', prefixedName);
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
const installed = await dirExists(installedSkillDir);
|
|
114
|
+
// A skill outside the current selection is not installed, and that is no
|
|
115
|
+
// longer a reason to refuse: skills are plugin-scoped now, so "not
|
|
116
|
+
// installed" is an ordinary state for a registry skill nobody selected.
|
|
117
|
+
// The shadow is seeded from the shipped source instead and reported as
|
|
118
|
+
// DORMANT — it exists, it is preserved by every future install, and it
|
|
119
|
+
// applies to nothing until the plugin that uses it is selected.
|
|
120
|
+
const seedDir = shadowSeedDir(bareName, installed ? installedSkillDir : null);
|
|
121
|
+
if (!await dirExists(seedDir)) {
|
|
122
|
+
p.log.error(`No source for ${bareName} — reinstall devflow, then try again.`);
|
|
77
123
|
process.exit(1);
|
|
78
124
|
}
|
|
79
125
|
const shadowDir = getShadowDir(devflowDir, bareName);
|
|
@@ -83,8 +129,12 @@ export const skillsCommand = new Command('skills')
|
|
|
83
129
|
}
|
|
84
130
|
// Create shadow directory (unprefixed) and copy original as reference backup
|
|
85
131
|
await fs.mkdir(path.join(devflowDir, 'skills'), { recursive: true });
|
|
86
|
-
await copyDirectory(
|
|
132
|
+
await copyDirectory(seedDir, shadowDir);
|
|
87
133
|
p.log.success(`Shadowed ${color.cyan(bareName)}`);
|
|
134
|
+
if (!installed) {
|
|
135
|
+
p.log.warn(`Shadow for ${bareName} is inactive — the plugin that uses it is not selected. ` +
|
|
136
|
+
`Run devflow init and select ${describeOwners(bareName)} to apply it.`);
|
|
137
|
+
}
|
|
88
138
|
p.log.info(`Edit ${color.dim(path.join(shadowDir, 'SKILL.md'))} then run devflow init to apply.`);
|
|
89
139
|
}
|
|
90
140
|
else if (action === 'unshadow') {
|
|
@@ -114,14 +164,27 @@ export const skillsCommand = new Command('skills')
|
|
|
114
164
|
catch { /* skills dir absent — no shadows */ }
|
|
115
165
|
const shadowDirSet = new Set(shadowDirNames);
|
|
116
166
|
const knownSkillSet = new Set(allSkills);
|
|
167
|
+
// L3: every skill's declarers, resolved ONCE into a map before any row is
|
|
168
|
+
// rendered. Calling skillOwners() inside the row loop would walk the whole
|
|
169
|
+
// registry per skill for an answer that does not change between rows.
|
|
170
|
+
const ownersBySkill = new Map(allSkills.map(skill => [skill, skillOwners(skill)]));
|
|
117
171
|
// Build rows in parallel; short-circuit validateSkillShadow for skills with no shadow dir
|
|
118
172
|
const knownResults = await Promise.all(allSkills.map(async (skill) => {
|
|
119
173
|
const shadowState = shadowDirSet.has(skill)
|
|
120
174
|
? await validateSkillShadow(path.join(shadowsRoot, skill))
|
|
121
175
|
: 'none';
|
|
122
|
-
|
|
176
|
+
const installed = await dirExists(path.join(claudeDir, 'skills', prefixSkillName(skill)));
|
|
177
|
+
return { skill, shadowState, installed };
|
|
123
178
|
}));
|
|
124
|
-
const rows = knownResults.map(({ skill, shadowState }) =>
|
|
179
|
+
const rows = knownResults.map(({ skill, shadowState, installed }) => {
|
|
180
|
+
// Skills are plugin-scoped, so "which plugin do I select to keep this?"
|
|
181
|
+
// is the question the list has to answer — and for a skill that is not
|
|
182
|
+
// installed it is the only useful thing the row can say.
|
|
183
|
+
const provenance = installed
|
|
184
|
+
? color.dim(`installed because: ${describeOwners(skill, ownersBySkill.get(skill))}`)
|
|
185
|
+
: color.dim(`not installed — provided by: ${describeOwners(skill, ownersBySkill.get(skill))}`);
|
|
186
|
+
return ` ${color.cyan(skill.padEnd(28))} ${buildSkillShadowTag(shadowState).padEnd(20)} ${provenance}`;
|
|
187
|
+
});
|
|
125
188
|
// Orphan shadows: in ~/.devflow/skills/ but not a known skill
|
|
126
189
|
for (const dirName of shadowDirNames) {
|
|
127
190
|
if (!knownSkillSet.has(dirName)) {
|
|
@@ -129,7 +192,8 @@ export const skillsCommand = new Command('skills')
|
|
|
129
192
|
}
|
|
130
193
|
}
|
|
131
194
|
const shadowedCount = knownResults.filter(r => r.shadowState !== 'none').length;
|
|
132
|
-
|
|
195
|
+
const installedCount = knownResults.filter(r => r.installed).length;
|
|
196
|
+
p.note(rows.join('\n'), `Skills (${allSkills.length} known, ${installedCount} installed, ${shadowedCount} shadowed)`);
|
|
133
197
|
}
|
|
134
198
|
else {
|
|
135
199
|
p.log.error(`Unknown action: ${action}`);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracker prompt helpers for devflow init.
|
|
3
|
+
*
|
|
4
|
+
* CLI-layer module (ADR-013): prompt-rendering logic lives in src/cli/commands/,
|
|
5
|
+
* core business logic stays in src/core/tracker.ts.
|
|
6
|
+
*
|
|
7
|
+
* avoids PF-029: the wizard gate keys on `modePromptShown` (was the Setup-mode
|
|
8
|
+
* p.select prompt actually shown?), never on the mode name, so --recommended
|
|
9
|
+
* (flag, no prompt) and the non-TTY fallback preserve their promptless contracts.
|
|
10
|
+
* avoids PF-014: runTrackerStep never calls process.exit() or throws — callers
|
|
11
|
+
* own the cancel idiom (p.cancel + process.exit(0)), keeping try/finally safe.
|
|
12
|
+
* The shared DI seam (PromptOutcome, WizardPromptIO, clackNote, clackSelect) is
|
|
13
|
+
* defined once in prompt-io.ts and imported here, never re-declared.
|
|
14
|
+
*
|
|
15
|
+
* D-TRACKER-GATE: this step copies COMPLIANCE's gate, not ATTRIBUTION's.
|
|
16
|
+
* Attribution is Advanced-only because it silently rewrites git metadata; a
|
|
17
|
+
* wrong tracker provider is immediately visible and trivially reversible, while
|
|
18
|
+
* a user who never sees the question silently gets `github` — invisible to
|
|
19
|
+
* exactly the Jira/Linear user the question exists for. Compliance's signature
|
|
20
|
+
* is also the only one already carrying `hasCliOverride`, which `--tracker`
|
|
21
|
+
* needs.
|
|
22
|
+
*/
|
|
23
|
+
import { clackNote, clackSelect } from './prompt-io.js';
|
|
24
|
+
import { DEFAULT_TRACKER_PROVIDER, TRACKER_PROVIDERS, } from '../../core/tracker.js';
|
|
25
|
+
// ── Shared prompt content ──────────────────────────────────────────────────────
|
|
26
|
+
/** Message shown on the tracker provider select prompt. */
|
|
27
|
+
export const TRACKER_SELECT_MESSAGE = 'Issue tracker for this machine';
|
|
28
|
+
/** Build clack select options for the tracker provider list. */
|
|
29
|
+
export function providerChoices() {
|
|
30
|
+
return TRACKER_PROVIDERS.map(provider => ({
|
|
31
|
+
value: provider.id,
|
|
32
|
+
label: provider.label,
|
|
33
|
+
hint: provider.hint,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a padded provider catalogue suitable for a clack note body.
|
|
38
|
+
* Produces: ` github — GitHub Issues through the gh CLI`
|
|
39
|
+
*/
|
|
40
|
+
export function formatProviderCatalogue() {
|
|
41
|
+
return 'Valid provider IDs:\n' +
|
|
42
|
+
TRACKER_PROVIDERS.map(p => ` ${p.id.padEnd(10)} — ${p.hint}`).join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format tracker state for the Recommended-mode summary line and note header.
|
|
46
|
+
*
|
|
47
|
+
* Pure function — no I/O, no side effects. `github` carries the `(default)`
|
|
48
|
+
* marker so a user reading the summary can tell "I chose this" from "this is
|
|
49
|
+
* what devflow does when nobody chooses".
|
|
50
|
+
*/
|
|
51
|
+
export function formatTrackerSummary(provider) {
|
|
52
|
+
return provider === DEFAULT_TRACKER_PROVIDER ? `${provider} (default)` : provider;
|
|
53
|
+
}
|
|
54
|
+
// ── Gate predicate ─────────────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Determines whether the tracker wizard step should run for a given init invocation.
|
|
57
|
+
*
|
|
58
|
+
* Gate table (per PF-029: key on modePromptShown, never on the mode name):
|
|
59
|
+
*
|
|
60
|
+
* --recommended flag / !isTTY fallback → no (promptless contract preserved)
|
|
61
|
+
* Interactive mode-prompt → Recommended → yes (modePromptShown=true)
|
|
62
|
+
* --advanced flag / re-init (banner path) → yes (mode='advanced', isTTY=true)
|
|
63
|
+
* Interactive mode-prompt → Advanced → yes (modePromptShown=true)
|
|
64
|
+
* Any path with --tracker <id> → no (hasCliOverride wins)
|
|
65
|
+
*
|
|
66
|
+
* BOTH wizard paths call this predicate, so the table above is the single
|
|
67
|
+
* authority for both and they cannot drift. A Recommended-only wiring would be
|
|
68
|
+
* dead on every re-init — re-init is Advanced-only by construction.
|
|
69
|
+
*
|
|
70
|
+
* Pure predicate — no side effects, fully testable without a TTY.
|
|
71
|
+
*/
|
|
72
|
+
export function shouldRunTrackerStep(input) {
|
|
73
|
+
if (input.hasCliOverride)
|
|
74
|
+
return false;
|
|
75
|
+
if (!input.isTTY)
|
|
76
|
+
return false;
|
|
77
|
+
// Advanced path: non-TTY has already exit-1'd, so isTTY=true here → always run.
|
|
78
|
+
// Covers: --advanced flag, re-init banner path, interactive-prompt → advanced.
|
|
79
|
+
if (input.mode === 'advanced')
|
|
80
|
+
return true;
|
|
81
|
+
// Recommended path: only run when the Setup-mode p.select actually ran
|
|
82
|
+
// (user made an active choice). --recommended flag and !isTTY fallback never set
|
|
83
|
+
// modePromptShown=true, preserving their promptless contracts.
|
|
84
|
+
return input.modePromptShown;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the real (clack) TrackerPromptIO adapter.
|
|
88
|
+
* Delegates the shared note + select to the shared adapters (prompt-io.ts).
|
|
89
|
+
* Translates clack's cancel symbol into the PromptOutcome discriminated union.
|
|
90
|
+
*/
|
|
91
|
+
export function buildClackTrackerPrompts() {
|
|
92
|
+
return {
|
|
93
|
+
note: clackNote,
|
|
94
|
+
select: (opts) => clackSelect(opts),
|
|
95
|
+
selectProvider: (opts) => clackSelect(opts),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the tracker wizard step.
|
|
100
|
+
*
|
|
101
|
+
* Flow:
|
|
102
|
+
* 1. Note — "Current setting: …" header then the provider catalogue.
|
|
103
|
+
* 2. Provider select — labelled GitHub / Jira / Linear with hints, seeded from
|
|
104
|
+
* the prior state. `p.select`, never `p.confirm`: Enter-through must be an
|
|
105
|
+
* INFORMED keep of a named provider, not a y/N reflex (PF-029).
|
|
106
|
+
*
|
|
107
|
+
* Returns:
|
|
108
|
+
* {kind:'resolved', state, messages} — step completed; `state` is the chosen
|
|
109
|
+
* TrackerFeatureState; `messages` are emitted by the caller.
|
|
110
|
+
* {kind:'cancelled'} — user pressed Escape; caller runs p.cancel + process.exit(0).
|
|
111
|
+
*
|
|
112
|
+
* Invariants (PF-014):
|
|
113
|
+
* - Never calls process.exit(), never throws.
|
|
114
|
+
* - The returned state is always a fresh object, never the seed.
|
|
115
|
+
* - All I/O is routed through the `prompts` parameter (injectable for tests).
|
|
116
|
+
*/
|
|
117
|
+
export async function runTrackerStep(opts) {
|
|
118
|
+
const { seed, prompts } = opts;
|
|
119
|
+
prompts.note(`Current setting: ${formatTrackerSummary(seed.provider)}\n\n` +
|
|
120
|
+
'Selects which issue tracker devflow reads and writes when a workflow\n' +
|
|
121
|
+
'needs an issue. GitHub is the default and needs no extra setup.\n' +
|
|
122
|
+
'Jira and Linear learn your project\'s issue conventions in the\n' +
|
|
123
|
+
'background at the next session start; pull requests stay on GitHub\n' +
|
|
124
|
+
'either way.\n\n' +
|
|
125
|
+
formatProviderCatalogue(), 'Issue tracker');
|
|
126
|
+
const outcome = await prompts.selectProvider({
|
|
127
|
+
message: TRACKER_SELECT_MESSAGE,
|
|
128
|
+
options: providerChoices(),
|
|
129
|
+
initialValue: seed.provider,
|
|
130
|
+
});
|
|
131
|
+
if (outcome.kind === 'cancel')
|
|
132
|
+
return { kind: 'cancelled' };
|
|
133
|
+
const provider = outcome.value;
|
|
134
|
+
return {
|
|
135
|
+
kind: 'resolved',
|
|
136
|
+
// Fresh object — never alias the seed the caller still holds.
|
|
137
|
+
state: { provider },
|
|
138
|
+
messages: [
|
|
139
|
+
provider === DEFAULT_TRACKER_PROVIDER
|
|
140
|
+
? { level: 'info', text: `Tracker: ${provider} (default) — change later with devflow tracker --set <id>` }
|
|
141
|
+
: { level: 'success', text: `Tracker: ${provider}` },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=tracker-prompts.js.map
|