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.
Files changed (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. 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 { mergeDenyList, stripUserDenyList, detectDenyState, DEVFLOW_HISTORICAL_DENY, removeManagedSettings, installManagedSettings, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
8
- import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
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
- // ─── Command ──────────────────────────────────────────────────────────────────
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
- let existing;
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 writeFileAtomicExclusive(userSettingsPath, stripped);
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: gate on detected.managed (parse-safe signal from detectDenyState),
161
- // not raw managedExists — avoids raw JSON.parse crash inside removeManagedSettings
162
- // when the managed file exists but is corrupt JSON. (SF5 — applies engineering.md
163
- // "never throw in business logic"; avoids PF partial-failure: user settings already
164
- // stripped but manifest sync would be skipped on uncaught throw.)
165
- if (detected.managed) {
166
- await removeManagedSettings(rootDir, true);
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
- if (!await dirExists(installedSkillDir)) {
76
- p.log.error(`Skill not installed: ${prefixedName}. Run devflow init first.`);
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(installedSkillDir, shadowDir);
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
- return { skill, shadowState };
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 }) => ` ${color.cyan(skill.padEnd(28))} ${buildSkillShadowTag(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
- p.note(rows.join('\n'), `Skills (${allSkills.length} known, ${shadowedCount} shadowed)`);
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