devflow-kit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Shared wizard prompt-IO seam for devflow init wizard steps.
3
3
  *
4
- * ADR-019 corollary (one-definition seam): PromptOutcome and WizardPromptIO
5
- * were byte-identical duplicates across attribution-prompts.ts and
6
- * compliance-prompts.ts (architecture-03 / consistency-06). They are defined
7
- * ONCE here and re-used via import.
4
+ * One-definition seam: PromptOutcome and WizardPromptIO were byte-identical
5
+ * duplicates across attribution-prompts.ts and compliance-prompts.ts
6
+ * (architecture-03 / consistency-06). They are defined ONCE here and re-used
7
+ * via import.
8
8
  *
9
9
  * D-PROMPT-IO: WizardPromptIO is the base DI seam for all two-action wizard
10
10
  * steps (note + boolean select). Modules that add a third prompt extend this
@@ -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')
@@ -157,21 +224,14 @@ export const securityCommand = new Command('security')
157
224
  p.log.info('No Devflow deny entries found in user settings');
158
225
  }
159
226
  }
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
- }
227
+ // Managed settings: the shared removal (parse-safe gate, never throws — SF5;
228
+ // a throw here would skip the manifest sync below after user settings were
229
+ // already stripped).
230
+ const managedMsg = describeManagedDenyRemoval(await removeManagedDenyList(rootDir, false));
231
+ if (managedMsg.level === 'warn')
232
+ p.log.warn(managedMsg.text);
233
+ else
234
+ p.log.info(managedMsg.text);
175
235
  await syncManifestFeature(devflowDir, 'security', 'none');
176
236
  return;
177
237
  }
@@ -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.
@@ -31,6 +32,25 @@ export async function hasShadow(skillName, devflowDir) {
31
32
  const dir = devflowDir ?? getDevFlowDirectory();
32
33
  return dirExists(getShadowDir(dir, skillName));
33
34
  }
35
+ /**
36
+ * Which plugin(s) a user must select to get a skill — rendered for a message.
37
+ *
38
+ * Every declarer, never the first: once install is scoped, the question this
39
+ * answers is "which plugin do I select to keep this?", and a first-wins answer
40
+ * names one plugin out of several that would each do (D-ALL-OWNERS).
41
+ * FEATURE_OWNED skills have no plugin owner at all and say so, because telling
42
+ * a user to select a plugin for `compliance` would send them looking for one
43
+ * that does not exist.
44
+ */
45
+ function describeOwners(bareName, owners) {
46
+ const declarers = owners ?? skillOwners(bareName);
47
+ if (declarers.length === 0) {
48
+ return FEATURE_OWNED_SKILLS.includes(bareName)
49
+ ? 'its feature (devflow compliance --enable)'
50
+ : 'no plugin';
51
+ }
52
+ return declarers.join(' or ');
53
+ }
34
54
  /** Render the shadow-state display tag for a skill. Exhaustive switch catches new states at compile time. */
35
55
  function buildSkillShadowTag(shadowState) {
36
56
  switch (shadowState) {
@@ -72,8 +92,16 @@ export const skillsCommand = new Command('skills')
72
92
  }
73
93
  const prefixedName = prefixSkillName(bareName);
74
94
  const installedSkillDir = path.join(claudeDir, 'skills', prefixedName);
75
- if (!await dirExists(installedSkillDir)) {
76
- p.log.error(`Skill not installed: ${prefixedName}. Run devflow init first.`);
95
+ const installed = await dirExists(installedSkillDir);
96
+ // A skill outside the current selection is not installed, and that is no
97
+ // longer a reason to refuse: skills are plugin-scoped now, so "not
98
+ // installed" is an ordinary state for a registry skill nobody selected.
99
+ // The shadow is seeded from the shipped source instead and reported as
100
+ // DORMANT — it exists, it is preserved by every future install, and it
101
+ // applies to nothing until the plugin that uses it is selected.
102
+ const seedDir = installed ? installedSkillDir : path.join(skillsDir(), bareName);
103
+ if (!await dirExists(seedDir)) {
104
+ p.log.error(`No source for ${bareName} — reinstall devflow, then try again.`);
77
105
  process.exit(1);
78
106
  }
79
107
  const shadowDir = getShadowDir(devflowDir, bareName);
@@ -83,8 +111,12 @@ export const skillsCommand = new Command('skills')
83
111
  }
84
112
  // Create shadow directory (unprefixed) and copy original as reference backup
85
113
  await fs.mkdir(path.join(devflowDir, 'skills'), { recursive: true });
86
- await copyDirectory(installedSkillDir, shadowDir);
114
+ await copyDirectory(seedDir, shadowDir);
87
115
  p.log.success(`Shadowed ${color.cyan(bareName)}`);
116
+ if (!installed) {
117
+ p.log.warn(`Shadow for ${bareName} is inactive — the plugin that uses it is not selected. ` +
118
+ `Run devflow init and select ${describeOwners(bareName)} to apply it.`);
119
+ }
88
120
  p.log.info(`Edit ${color.dim(path.join(shadowDir, 'SKILL.md'))} then run devflow init to apply.`);
89
121
  }
90
122
  else if (action === 'unshadow') {
@@ -114,14 +146,27 @@ export const skillsCommand = new Command('skills')
114
146
  catch { /* skills dir absent — no shadows */ }
115
147
  const shadowDirSet = new Set(shadowDirNames);
116
148
  const knownSkillSet = new Set(allSkills);
149
+ // L3: every skill's declarers, resolved ONCE into a map before any row is
150
+ // rendered. Calling skillOwners() inside the row loop would walk the whole
151
+ // registry per skill for an answer that does not change between rows.
152
+ const ownersBySkill = new Map(allSkills.map(skill => [skill, skillOwners(skill)]));
117
153
  // Build rows in parallel; short-circuit validateSkillShadow for skills with no shadow dir
118
154
  const knownResults = await Promise.all(allSkills.map(async (skill) => {
119
155
  const shadowState = shadowDirSet.has(skill)
120
156
  ? await validateSkillShadow(path.join(shadowsRoot, skill))
121
157
  : 'none';
122
- return { skill, shadowState };
158
+ const installed = await dirExists(path.join(claudeDir, 'skills', prefixSkillName(skill)));
159
+ return { skill, shadowState, installed };
123
160
  }));
124
- const rows = knownResults.map(({ skill, shadowState }) => ` ${color.cyan(skill.padEnd(28))} ${buildSkillShadowTag(shadowState)}`);
161
+ const rows = knownResults.map(({ skill, shadowState, installed }) => {
162
+ // Skills are plugin-scoped, so "which plugin do I select to keep this?"
163
+ // is the question the list has to answer — and for a skill that is not
164
+ // installed it is the only useful thing the row can say.
165
+ const provenance = installed
166
+ ? color.dim(`installed because: ${describeOwners(skill, ownersBySkill.get(skill))}`)
167
+ : color.dim(`not installed — provided by: ${describeOwners(skill, ownersBySkill.get(skill))}`);
168
+ return ` ${color.cyan(skill.padEnd(28))} ${buildSkillShadowTag(shadowState).padEnd(20)} ${provenance}`;
169
+ });
125
170
  // Orphan shadows: in ~/.devflow/skills/ but not a known skill
126
171
  for (const dirName of shadowDirNames) {
127
172
  if (!knownSkillSet.has(dirName)) {
@@ -129,7 +174,8 @@ export const skillsCommand = new Command('skills')
129
174
  }
130
175
  }
131
176
  const shadowedCount = knownResults.filter(r => r.shadowState !== 'none').length;
132
- p.note(rows.join('\n'), `Skills (${allSkills.length} known, ${shadowedCount} shadowed)`);
177
+ const installedCount = knownResults.filter(r => r.installed).length;
178
+ p.note(rows.join('\n'), `Skills (${allSkills.length} known, ${installedCount} installed, ${shadowedCount} shadowed)`);
133
179
  }
134
180
  else {
135
181
  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