devflow-kit 2.3.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 (169) hide show
  1. package/CHANGELOG.md +177 -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 +144 -0
  6. package/dist/cli/commands/compliance-prompts.js +7 -11
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +69 -30
  9. package/dist/cli/commands/init.js +551 -77
  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 +38 -0
  16. package/dist/cli/commands/proxy.js +91 -31
  17. package/dist/cli/commands/security.js +76 -16
  18. package/dist/cli/commands/skills.js +53 -7
  19. package/dist/cli/commands/tracker-prompts.js +145 -0
  20. package/dist/cli/commands/tracker.js +405 -0
  21. package/dist/cli/commands/uninstall.js +233 -77
  22. package/dist/cli.js +2 -0
  23. package/dist/commands/bug-analysis.md +22 -4
  24. package/dist/commands/code-review.md +44 -15
  25. package/dist/commands/debug.md +20 -6
  26. package/dist/commands/dynamic-build.md +289 -67
  27. package/dist/commands/dynamic-plan.md +60 -21
  28. package/dist/commands/dynamic-profile.md +1 -1
  29. package/dist/commands/dynamic-tickets.md +58 -8
  30. package/dist/commands/explore.md +2 -2
  31. package/dist/commands/implement.md +241 -53
  32. package/dist/commands/plan.md +88 -17
  33. package/dist/commands/release.md +64 -17
  34. package/dist/commands/resolve.md +138 -58
  35. package/dist/commands/self-review.md +2 -2
  36. package/dist/core/agent-models.js +55 -12
  37. package/dist/core/assets.js +58 -2
  38. package/dist/core/evidence-policy.js +147 -0
  39. package/dist/core/feature-config.js +130 -64
  40. package/dist/core/feature-switch.js +112 -0
  41. package/dist/core/flags.js +149 -8
  42. package/dist/core/manifest.js +33 -7
  43. package/dist/core/mds-variants.js +861 -0
  44. package/dist/core/model-discovery.js +12 -1
  45. package/dist/core/plugins.js +357 -9
  46. package/dist/core/project-paths.js +1 -1
  47. package/dist/core/proxy-log.js +8 -4
  48. package/dist/core/proxy-state.js +100 -43
  49. package/dist/core/reference-sweep.js +136 -0
  50. package/dist/core/tracker.js +407 -0
  51. package/dist/skills/git/references/decision-markers.md +19 -0
  52. package/dist/skills/git/references/learn-conventions.md +56 -0
  53. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  54. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  55. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  56. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  57. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  58. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  59. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  60. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  61. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  62. package/dist/skills/git/references/publication-gate.md +13 -0
  63. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  64. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  65. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  66. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  67. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  68. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  70. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  71. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  72. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  73. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  74. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  75. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  76. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  77. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  79. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  81. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  82. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  83. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  84. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  85. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  86. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  87. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  88. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  90. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  92. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  93. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  94. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  95. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  96. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  97. package/dist/skills/git/references/trust-rule.md +7 -0
  98. package/dist/targets/claude-code/installer.js +1213 -31
  99. package/dist/targets/claude-code/legacy.js +5 -0
  100. package/dist/targets/claude-code/post-install.js +303 -104
  101. package/dist/targets/claude-code/tracker-install.js +161 -0
  102. package/package.json +4 -3
  103. package/src/assets/agents/code.md +42 -4
  104. package/src/assets/agents/design.md +1 -1
  105. package/src/assets/agents/git.mds +827 -0
  106. package/src/assets/agents/knowledge.md +1 -1
  107. package/src/assets/agents/learning.md +11 -0
  108. package/src/assets/agents/synthesize.md +1 -1
  109. package/src/assets/agents/test.md +16 -5
  110. package/src/assets/agents/tracker.md +467 -0
  111. package/src/assets/agents/validate.md +7 -5
  112. package/src/assets/commands/_partials/_engine.mds +11 -9
  113. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  114. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  115. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  116. package/src/assets/commands/_partials/_preamble.mds +1 -1
  117. package/src/assets/commands/_partials/_publication.mds +3 -1
  118. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  119. package/src/assets/commands/_partials/_tracker.mds +18 -0
  120. package/src/assets/commands/_partials/_wave.mds +16 -10
  121. package/src/assets/commands/bug-analysis.mds +15 -5
  122. package/src/assets/commands/code-review.mds +34 -14
  123. package/src/assets/commands/debug.mds +11 -4
  124. package/src/assets/commands/dynamic-build.mds +227 -41
  125. package/src/assets/commands/dynamic-plan.mds +35 -13
  126. package/src/assets/commands/dynamic-tickets.mds +47 -5
  127. package/src/assets/commands/implement.mds +206 -52
  128. package/src/assets/commands/plan.mds +70 -17
  129. package/src/assets/commands/release.md +64 -17
  130. package/src/assets/commands/resolve.mds +126 -56
  131. package/src/assets/mds/git/_pr.mds +331 -0
  132. package/src/assets/mds/git/_references.mds +135 -0
  133. package/src/assets/mds/tracker/_common.mds +156 -0
  134. package/src/assets/mds/tracker/_github.mds +472 -0
  135. package/src/assets/mds/tracker/_jira.mds +407 -0
  136. package/src/assets/mds/tracker/_linear.mds +449 -0
  137. package/src/assets/mds/tracker/_mcp.mds +299 -0
  138. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  139. package/src/assets/scripts/hooks/background-memory-update +14 -9
  140. package/src/assets/scripts/hooks/capture-prompt +6 -2
  141. package/src/assets/scripts/hooks/capture-question +6 -2
  142. package/src/assets/scripts/hooks/capture-turn +6 -2
  143. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  144. package/src/assets/scripts/hooks/ensure-proxy +64 -5
  145. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  146. package/src/assets/scripts/hooks/hook-log-init +3 -1
  147. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  149. package/src/assets/scripts/hooks/memory-worker +15 -8
  150. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  151. package/src/assets/scripts/hooks/preamble +1 -4
  152. package/src/assets/scripts/hooks/queue-append +68 -24
  153. package/src/assets/scripts/hooks/session-start-context +355 -8
  154. package/src/assets/scripts/hooks/session-start-memory +12 -8
  155. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  156. package/src/assets/scripts/redact-secrets.cjs +490 -62
  157. package/src/assets/scripts/release-trace.cjs +1143 -0
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  159. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  160. package/src/assets/skills/compliance/SKILL.md +2 -0
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  162. package/src/assets/skills/git/SKILL.md +8 -82
  163. package/src/assets/skills/git/references/github-api.md +179 -141
  164. package/src/assets/skills/git/references/patterns.md +11 -12
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  167. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  168. package/src/targets/claude-code/templates/settings.json +0 -4
  169. package/src/assets/agents/git.md +0 -938
@@ -6,7 +6,9 @@ import * as p from '@clack/prompts';
6
6
  import color from 'picocolors';
7
7
  import { getInstallationPaths, getClaudeDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
8
8
  import { getGitRoot } from '../../core/git.js';
9
- import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
9
+ import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, skillsOf, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
10
+ import { readManifest } from '../../core/manifest.js';
11
+ import { TRACKER_ATTEMPTS_FILE, TRACKER_CLAIM_FILE, TRACKER_ENABLED_FILE } from '../../core/tracker.js';
10
12
  import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
11
13
  import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
12
14
  import { removeAmbientHook } from './ambient.js';
@@ -15,9 +17,10 @@ import { removeCaptureHooks } from './capture.js';
15
17
  import { removeDreamHook } from './legacy-hooks.js';
16
18
  import { removeHudStatusLine } from './hud.js';
17
19
  import { removeContextHook } from './context.js';
18
- import { applyDisableToSettings } from './proxy.js';
19
- import { readProxyState, DEFAULT_PROXY_PORT } from '../../core/proxy-state.js';
20
+ import { applyProxyTeardownToSettings } from './proxy.js';
21
+ import { readProxyState, proxyJsonExists } from '../../core/proxy-state.js';
20
22
  import { hudCacheDir } from '../../core/cache.js';
23
+ import { TRACKER_CONVENTIONS_FILE, TRACKER_CONVENTIONS_BACKUP_NAMES, TRACKER_STAGED_PREFIX, } from '../../core/tracker.js';
21
24
  import { revertExternalAgents } from '../../core/agent-models.js';
22
25
  import { detectShell, getProfilePath } from '../../core/safe-delete.js';
23
26
  import { isAlreadyInstalled, removeFromProfile } from '../../core/safe-delete-install.js';
@@ -26,20 +29,51 @@ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
26
29
  import { stripFlags } from '../../core/flags.js';
27
30
  import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
28
31
  import { getPackageRoot, isContainedIn } from '../../core/paths.js';
32
+ /**
33
+ * The plugins the manifest records as installed, as registry definitions.
34
+ *
35
+ * Falls back to the whole registry when there is no readable manifest, or when
36
+ * it names nothing this registry still has: that is the pre-manifest and the
37
+ * corrupt-manifest case, and retaining too much is the safe direction for a
38
+ * removal. Names the manifest carries that the registry has since dropped are
39
+ * skipped rather than invented — a definition is what the retained-set
40
+ * arithmetic needs, and there is none for a deleted plugin.
41
+ */
42
+ export async function resolveInstalledPlugins(devflowDir) {
43
+ const manifest = await readManifest(devflowDir).catch(() => null);
44
+ const names = new Set(manifest?.plugins ?? []);
45
+ if (names.size === 0)
46
+ return DEVFLOW_PLUGINS;
47
+ const resolved = DEVFLOW_PLUGINS.filter(plugin => names.has(plugin.name));
48
+ return resolved.length === 0 ? DEVFLOW_PLUGINS : resolved;
49
+ }
29
50
  /**
30
51
  * Compute which assets should be removed during selective plugin uninstall.
31
52
  * Skills and agents shared by remaining plugins are retained.
32
53
  * Rules shared by remaining plugins are also retained.
54
+ *
55
+ * D-RETAIN-FROM-MANIFEST: `installedPlugins` is what the MANIFEST records as
56
+ * installed, not the whole registry. Once skills are plugin-scoped the two stop
57
+ * agreeing, and taking the registry retains assets on behalf of plugins the user
58
+ * never installed — so `devflow uninstall --plugin=X` keeps X's skills alive
59
+ * because some unselected plugin also declares them, and the user is left with
60
+ * exactly the files they asked to remove. Skills are retained across the CLOSURE
61
+ * (`skills ∪ requires`) of the remaining plugins, for the same reason the
62
+ * install set is a closure: a skill another installed plugin merely requires is
63
+ * still a skill it needs.
64
+ *
65
+ * @param installedPlugins - The plugins the manifest records. Callers pass the
66
+ * registry only when there is no manifest to read, and the dry-run and the
67
+ * real removal must always be given the SAME list or they describe different
68
+ * outcomes.
33
69
  */
34
- export function computeAssetsToRemove(selectedPlugins, allPlugins) {
70
+ export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
35
71
  const selectedNames = new Set(selectedPlugins.map(p => p.name));
36
- const remainingPlugins = allPlugins.filter(p => !selectedNames.has(p.name));
37
- const retainedSkills = new Set();
72
+ const remainingPlugins = installedPlugins.filter(p => !selectedNames.has(p.name));
73
+ const retainedSkills = skillsOf(remainingPlugins);
38
74
  const retainedAgents = new Set();
39
75
  const retainedRules = new Set();
40
76
  for (const rp of remainingPlugins) {
41
- for (const s of rp.skills)
42
- retainedSkills.add(s);
43
77
  for (const a of rp.agents)
44
78
  retainedAgents.add(a);
45
79
  for (const r of rp.rules)
@@ -49,11 +83,11 @@ export function computeAssetsToRemove(selectedPlugins, allPlugins) {
49
83
  const agents = [];
50
84
  const commands = [];
51
85
  const rules = [];
86
+ for (const skill of skillsOf(selectedPlugins)) {
87
+ if (!retainedSkills.has(skill))
88
+ skills.push(skill);
89
+ }
52
90
  for (const plugin of selectedPlugins) {
53
- for (const skill of plugin.skills) {
54
- if (!retainedSkills.has(skill))
55
- skills.push(skill);
56
- }
57
91
  for (const agent of plugin.agents) {
58
92
  if (!retainedAgents.has(agent))
59
93
  agents.push(agent);
@@ -183,21 +217,69 @@ export function resolveDevflowDirCleanup(opts) {
183
217
  return 'prompt';
184
218
  }
185
219
  /**
186
- * Enumerate user-authored content in devflowDir that would be deleted by a
187
- * full cleanup of the directory.
220
+ * Single source of truth for user-authored content under `devflowDir`.
188
221
  *
189
- * Checks for items that exist on disk and are worth backing up:
190
- * devflowDir/skills/ — skill shadow overrides (user-maintained)
191
- * devflowDir/rules/ — rule shadow overrides (user-maintained)
192
- * devflowDir/preference-profile.md — dynamic-plan preference profile
193
- * devflowDir/learning.json — global learning agent tuning config
194
- * devflowDir/hud.json — HUD enable/disable preference and display config
222
+ * The counterpart to `installArtifactPaths`: together the two lists describe
223
+ * every Devflow-created entry in `~/.devflow`, and they never overlap (@D8).
224
+ * Membership here means "survives every path but a confirmed full-dir rm";
225
+ * membership there means "removed on decline, cancel, non-interactive and
226
+ * --keep-docs alike".
195
227
  *
196
228
  * NOT listed here: agent-models.json — reclassified as an INSTALL ARTIFACT (AC-P1-F4).
197
229
  * Stale per-agent model overrides silently re-apply to renamed/deleted agents on reinstall,
198
230
  * so it must be cleaned up by removeDevFlowInstallArtifacts, not preserved behind a confirm gate.
199
231
  *
200
- * Returns labels for each item that actually exists. Empty array means nothing
232
+ * @param devflowDir - Absolute path to ~/.devflow (labels quote the shadow dirs).
233
+ */
234
+ export function userContentPaths(devflowDir) {
235
+ return [
236
+ // Skill shadow overrides (~/.devflow/skills/{name}/)
237
+ { relPath: 'skills', isDir: true, label: `skill shadows (${path.join(devflowDir, 'skills')})` },
238
+ // Rule shadow overrides (~/.devflow/rules/{name}.md)
239
+ { relPath: 'rules', isDir: true, label: `rule shadows (${path.join(devflowDir, 'rules')})` },
240
+ // preference-profile.md — user-curated decision-preference profile
241
+ { relPath: 'preference-profile.md', label: 'preference-profile.md' },
242
+ // tracker.md — the inferred, hand-editable issue-tracker conventions file.
243
+ //
244
+ // USER CONTENT (OD-15), classified the same way as preference-profile.md above
245
+ // rather than as an install artifact like agent-models.json, because it is
246
+ // inferred ONCE per machine and then hand-editable: absence is the trigger that
247
+ // re-runs inference, so deleting it on every decline/cancel/--keep-docs path
248
+ // would silently discard work the user may have corrected by hand.
249
+ //
250
+ // REVERSAL CONDITION, recorded: this classification is CONDITIONAL on the
251
+ // provider-mismatch guard shipping. agent-models.json was reclassified to an
252
+ // artifact precisely because stale overrides re-apply *silently*; "silently" is
253
+ // the load-bearing word. A stale tracker.md whose frontmatter provider
254
+ // disagrees with the resolved provider produces
255
+ // `TRACEABILITY: DEGRADED (tracker configuration mismatch (conventions file))`
256
+ // and no tracker call — that is what removes the silence, and the reason names
257
+ // THIS file rather than the per-repo override so the user is told which of the
258
+ // two to edit. If that guard is ever dropped,
259
+ // reclassify tracker.md to an install artifact IN THE SAME CHANGE, otherwise a
260
+ // silently-authoritative stale file survives uninstall.
261
+ { relPath: TRACKER_CONVENTIONS_FILE, label: `${TRACKER_CONVENTIONS_FILE} (issue tracker conventions)` },
262
+ // tracker.md.{provider}.bak — what renameStaleTrackerConventions leaves behind
263
+ // on a provider change. Same inferred content as tracker.md (the user's site
264
+ // and project key), so the same classification: named by the confirm prompt,
265
+ // kept by every artifacts-only pass. The names come from the registry, so a
266
+ // fourth provider is covered the day it lands.
267
+ ...TRACKER_CONVENTIONS_BACKUP_NAMES.map(name => ({
268
+ relPath: name,
269
+ label: `${name} (previous issue tracker conventions)`,
270
+ })),
271
+ // learning.json — global learning agent tuning config
272
+ { relPath: 'learning.json', label: 'learning.json' },
273
+ // hud.json — user HUD enable/disable preference and display config
274
+ { relPath: 'hud.json', label: 'hud.json (HUD configuration)' },
275
+ ];
276
+ }
277
+ /**
278
+ * Enumerate user-authored content in devflowDir that would be deleted by a
279
+ * full cleanup of the directory.
280
+ *
281
+ * Walks `userContentPaths` and returns the label of every entry that exists on
282
+ * disk — a directory counts only when it is non-empty. Empty array means nothing
201
283
  * user-authored is present in the directory.
202
284
  *
203
285
  * Pure I/O — no side effects, no output, fully testable.
@@ -207,41 +289,54 @@ export function resolveDevflowDirCleanup(opts) {
207
289
  */
208
290
  export async function enumerateUserDevFlowContent(devflowDir) {
209
291
  const items = [];
210
- // Skill shadow overrides (~/.devflow/skills/{name}/)
211
- try {
212
- const entries = await fs.readdir(path.join(devflowDir, 'skills'));
213
- if (entries.length > 0) {
214
- items.push(`skill shadows (${path.join(devflowDir, 'skills')})`);
292
+ for (const entry of userContentPaths(devflowDir)) {
293
+ const fullPath = path.join(devflowDir, entry.relPath);
294
+ try {
295
+ if (entry.isDir) {
296
+ const children = await fs.readdir(fullPath);
297
+ if (children.length === 0)
298
+ continue;
299
+ }
300
+ else {
301
+ await fs.access(fullPath);
302
+ }
303
+ items.push(entry.label);
215
304
  }
305
+ catch { /* absent or unreadable */ }
216
306
  }
217
- catch { /* dir absent or unreadable */ }
218
- // Rule shadow overrides (~/.devflow/rules/{name}.md)
219
- try {
220
- const entries = await fs.readdir(path.join(devflowDir, 'rules'));
221
- if (entries.length > 0) {
222
- items.push(`rule shadows (${path.join(devflowDir, 'rules')})`);
307
+ return items;
308
+ }
309
+ /**
310
+ * Expand `installArtifactPaths` against real disk.
311
+ *
312
+ * Exact entries pass through untouched; a prefix entry becomes one entry per
313
+ * matching direct child of `devflowDir`. This is the ONE place a prefix turns
314
+ * into paths, so the removal loop and the dry-run preview cannot disagree about
315
+ * what a prefix covers. An unreadable or absent `devflowDir` yields the exact
316
+ * entries alone — absent is the ordinary case here, never an error.
317
+ */
318
+ export async function resolveInstallArtifactPaths(devflowDir) {
319
+ const resolved = [];
320
+ let children = null;
321
+ for (const entry of installArtifactPaths(devflowDir)) {
322
+ if (entry.isPrefix !== true) {
323
+ resolved.push({ relPath: entry.relPath, isDir: entry.isDir });
324
+ continue;
325
+ }
326
+ if (children === null) {
327
+ try {
328
+ children = await fs.readdir(devflowDir);
329
+ }
330
+ catch {
331
+ children = [];
332
+ }
333
+ }
334
+ for (const child of children) {
335
+ if (child.startsWith(entry.relPath))
336
+ resolved.push({ relPath: child, isDir: entry.isDir });
223
337
  }
224
338
  }
225
- catch { /* dir absent or unreadable */ }
226
- // preference-profile.md — user-curated decision-preference profile
227
- try {
228
- await fs.access(path.join(devflowDir, 'preference-profile.md'));
229
- items.push('preference-profile.md');
230
- }
231
- catch { /* absent */ }
232
- // learning.json — global learning agent tuning config
233
- try {
234
- await fs.access(path.join(devflowDir, 'learning.json'));
235
- items.push('learning.json');
236
- }
237
- catch { /* absent */ }
238
- // hud.json — user HUD enable/disable preference and display config
239
- try {
240
- await fs.access(path.join(devflowDir, 'hud.json'));
241
- items.push('hud.json (HUD configuration)');
242
- }
243
- catch { /* absent */ }
244
- return items;
339
+ return resolved;
245
340
  }
246
341
  /**
247
342
  * Single source of truth for Devflow-owned install artifacts under `devflowDir`.
@@ -252,9 +347,18 @@ export async function enumerateUserDevFlowContent(devflowDir) {
252
347
  * lets callers (dry-run display, tests) enumerate the artifact set without
253
348
  * duplicating the list.
254
349
  *
255
- * @D8 Nothing returned here may overlap with the items enumerated by
256
- * `enumerateUserDevFlowContent` — the disjointness invariant is tested by test 9f
257
- * and enforced by keeping both lists in one place.
350
+ * @D8 Nothing returned here may overlap with `userContentPaths` — the two lists
351
+ * are the whole of what Devflow puts in `~/.devflow`, and a name in both is
352
+ * deleted whatever the user answers to the full-wipe prompt. Checked two ways:
353
+ * mechanically, by intersecting the `relPath` sets of the two functions and
354
+ * checking no user path falls UNDER a prefix entry (so an entry added to either
355
+ * list is covered the day it lands), and behaviourally, by test 9f — every
356
+ * enumerated user item survives an artifact-only removal.
357
+ *
358
+ * Entries marked `isPrefix` name a FAMILY of per-invocation basenames rather
359
+ * than one path; `resolveInstallArtifactPaths` is what turns them into paths.
360
+ * Every consumer goes through that resolver, so a prefix entry is never treated
361
+ * as a literal filename.
258
362
  *
259
363
  * @param devflowDir - Absolute path to ~/.devflow (used to resolve cache dir).
260
364
  */
@@ -274,6 +378,20 @@ export function installArtifactPaths(devflowDir) {
274
378
  { relPath: 'proxy-routing.json' },
275
379
  { relPath: 'proxy.pid' },
276
380
  { relPath: '.proxy-spawn.lock', isDir: true },
381
+ // tracker runtime artifacts — the Tracker agent's atomic claim file, its
382
+ // inference attempt counter, and the provider presence sentinel the
383
+ // SessionStart hook stats. All three are machine state with no user-authored
384
+ // content, so they go on this list; `tracker.md` beside them and the
385
+ // `tracker.md.{provider}.bak` a provider change leaves are USER CONTENT
386
+ // (OD-15) and are deliberately NOT here (@D8: the two lists stay disjoint).
387
+ { relPath: TRACKER_CLAIM_FILE },
388
+ { relPath: TRACKER_ATTEMPTS_FILE },
389
+ { relPath: TRACKER_ENABLED_FILE },
390
+ // The agent's scrubbed staging file, one per invocation under a mktemp name
391
+ // it removes from a trap — a SIGKILL outruns the trap and leaves it behind.
392
+ // A prefix, because the names exist only on disk. Content is a scrubbed copy
393
+ // that was never placed, so it is machine state like the three above.
394
+ { relPath: TRACKER_STAGED_PREFIX, isPrefix: true },
277
395
  // per-project hook logs (logs/{project-slug}/) AND global logs — remove the
278
396
  // whole logs/ tree; covers proxy.log, debug logs, and any project-slug dirs.
279
397
  { relPath: 'logs', isDir: true },
@@ -295,7 +413,8 @@ export function installArtifactPaths(devflowDir) {
295
413
  * @D8 Nothing enumerated by enumerateUserDevFlowContent may appear in this list.
296
414
  * This function runs on the decline, cancel, non-interactive AND --keep-docs paths,
297
415
  * so an entry here is deleted even when the user answers "no" to the full wipe.
298
- * User-authored state (skill/rule shadows, preference-profile.md, learning.json,
416
+ * User-authored state (everything in `userContentPaths`: skill/rule shadows,
417
+ * preference-profile.md, tracker.md and its provider backups, learning.json,
299
418
  * hud.json) is removed only by the confirmed full-dir rm.
300
419
  * agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
301
420
  * re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
@@ -327,8 +446,10 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
327
446
  }
328
447
  }
329
448
  catch { /* proxy.pid absent or unreadable — non-fatal */ }
330
- // All install artifacts removed non-fatally (avoids PF-009).
331
- for (const artifact of installArtifactPaths(devflowDir)) {
449
+ // All install artifacts removed non-fatally (avoids PF-009). Resolved against
450
+ // disk first, so a per-run staging basename is a real path by the time the
451
+ // containment guard below sees it.
452
+ for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
332
453
  const fullPath = path.join(devflowDir, artifact.relPath);
333
454
  // Containment invariant: every artifact must resolve to a path STRICTLY inside
334
455
  // devflowDir. A derived relPath that ever collapsed to '' or '..' would turn the
@@ -456,7 +577,7 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
456
577
  // Guard with fs.access so files that never existed don't pollute the preview.
457
578
  // (F7: previously pushed unconditionally, inflating the dry-run list with
458
579
  // paths that were never on disk.)
459
- for (const artifact of installArtifactPaths(devflowDir)) {
580
+ for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
460
581
  const fullPath = path.join(devflowDir, artifact.relPath);
461
582
  try {
462
583
  await fs.access(fullPath);
@@ -476,13 +597,18 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
476
597
  * @param opts.scopesToUninstall - Scopes detected in the setup phase.
477
598
  * @param opts.isSelectiveUninstall - true when --plugin was given.
478
599
  * @param opts.selectedPlugins - The plugin subset for selective mode.
600
+ * @param opts.installedPlugins - What the manifest records as installed. The
601
+ * dry-run and the real removal MUST receive the same list, or the preview
602
+ * describes an outcome the removal does not produce.
479
603
  */
480
604
  export async function runDryRunPhase(opts) {
481
- const { scopesToUninstall, isSelectiveUninstall, selectedPlugins } = opts;
605
+ const { scopesToUninstall, isSelectiveUninstall, selectedPlugins, installedPlugins } = opts;
482
606
  p.log.info(`Scope(s): ${[...scopesToUninstall].join(', ')} (dry-run shows all detected scopes)`);
483
607
  if (isSelectiveUninstall) {
484
- // Selective: compute from registry — this accurately reflects what would be removed.
485
- const assets = computeAssetsToRemove(selectedPlugins, DEVFLOW_PLUGINS);
608
+ // Selective: computed against the INSTALLED list, the same argument the real
609
+ // removal is given below — a preview computed from a different list is a
610
+ // preview of a different uninstall.
611
+ const assets = computeAssetsToRemove(selectedPlugins, installedPlugins);
486
612
  const plan = formatDryRunPlan(assets);
487
613
  for (const line of plan.split('\n')) {
488
614
  p.log.info(line);
@@ -527,6 +653,7 @@ export async function runDryRunPhase(opts) {
527
653
  */
528
654
  export async function runSelectivePhaseForScope(opts) {
529
655
  const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
656
+ const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
530
657
  // Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
531
658
  // lines from installed agent frontmatter while the files are still present.
532
659
  // Non-fatal: tolerate missing agents dir or revert errors.
@@ -543,7 +670,7 @@ export async function runSelectivePhaseForScope(opts) {
543
670
  }
544
671
  catch { /* agents dir absent or revert failed — non-fatal */ }
545
672
  }
546
- await removeSelectedPlugins(claudeDir, selectedPlugins, verbose);
673
+ await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins);
547
674
  // Clean up ambient hook if ambient plugin is being removed
548
675
  if (selectedPlugins.some(sp => sp.name === 'devflow-ambient')) {
549
676
  const settingsPath = path.join(claudeDir, 'settings.json');
@@ -768,14 +895,16 @@ export async function runCleanupPhase(opts) {
768
895
  settingsContent = stripFlags(settingsContent); // also strips viewMode via view-mode registry entry
769
896
  settingsContent = stripDevflowTeammateModeFromJson(settingsContent);
770
897
  // Remove proxy hooks and ANTHROPIC_BASE_URL env in a single parse-mutate-serialize pass.
771
- // REG-1: scope the URL strip to the port Devflow manages — use the pre-captured port
772
- // (from opts.managedProxyPorts) because proxy.json is already deleted by
773
- // removeDevFlowInstallArtifacts before this phase runs. A user's own localhost
774
- // gateway on any other port is left in settings untouched.
898
+ // D-STRIP-1: an absent entry in managedProxyPorts means proxy.json did not exist
899
+ // before artifact removal — no evidence Devflow ever wrote the env vars, so
900
+ // applyProxyTeardownToSettings removes only our hooks and leaves the user's
901
+ // ANTHROPIC_BASE_URL / CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT alone.
902
+ // Hook removal is never gated: our hooks must not survive an uninstall.
903
+ // REG-1: the port-scoped URL strip protects any foreign localhost gateway on a
904
+ // different port — the managedPort value is from the pre-captured proxy.json port.
775
905
  {
776
- const managedPort = opts.managedProxyPorts?.get(scope) ?? DEFAULT_PROXY_PORT;
777
906
  const parsedSettings = JSON.parse(settingsContent);
778
- applyDisableToSettings(parsedSettings, managedPort);
907
+ applyProxyTeardownToSettings(parsedSettings, opts.managedProxyPorts?.get(scope));
779
908
  settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
780
909
  }
781
910
  if (settingsContent !== originalContent) {
@@ -970,23 +1099,44 @@ export const uninstallCommand = new Command('uninstall')
970
1099
  }
971
1100
  // === DRY RUN: show plan and exit ===
972
1101
  if (dryRun) {
973
- await runDryRunPhase({ scopesToUninstall, isSelectiveUninstall, selectedPlugins });
1102
+ // One resolution, handed to the dry-run and (below) to the real removal,
1103
+ // so the preview and the outcome are computed from the same list.
1104
+ let dryRunInstalled = DEVFLOW_PLUGINS;
1105
+ try {
1106
+ const paths = await getInstallationPaths(scopesToUninstall[0]);
1107
+ dryRunInstalled = await resolveInstalledPlugins(paths.devflowDir);
1108
+ }
1109
+ catch { /* scope path resolution failed — fall back to the registry */ }
1110
+ await runDryRunPhase({
1111
+ scopesToUninstall,
1112
+ isSelectiveUninstall,
1113
+ selectedPlugins,
1114
+ installedPlugins: dryRunInstalled,
1115
+ });
974
1116
  p.outro(color.dim('No changes made (dry run)'));
975
1117
  return;
976
1118
  }
977
1119
  // Pre-capture managed proxy ports for each scope BEFORE artifact removal.
978
1120
  // proxy.json is deleted by removeDevFlowInstallArtifacts (inside runFullPhaseForScope),
979
1121
  // so reading it after the full phase would always yield DEFAULT_PROXY_PORT. (F6)
1122
+ //
1123
+ // D-STRIP-1: only populate managedProxyPorts when proxy.json exists — evidence that
1124
+ // Devflow has managed the proxy on this machine. runCleanupPhase gates the
1125
+ // applyProxyTeardownToSettings call on the port being present in the map, so when the file
1126
+ // is absent (never managed), no env strip is attempted and a user's own gateway vars
1127
+ // are left untouched.
980
1128
  const managedProxyPorts = new Map();
981
1129
  if (!isSelectiveUninstall) {
982
1130
  for (const scope of scopesToUninstall) {
983
1131
  try {
984
1132
  const paths = await getInstallationPaths(scope);
985
- const proxyState = await readProxyState(paths.devflowDir);
986
- if (proxyState.ok)
987
- managedProxyPorts.set(scope, proxyState.value.port);
1133
+ if (await proxyJsonExists(paths.devflowDir)) {
1134
+ const proxyState = await readProxyState(paths.devflowDir);
1135
+ if (proxyState.ok)
1136
+ managedProxyPorts.set(scope, proxyState.value.port);
1137
+ }
988
1138
  }
989
- catch { /* non-fatal: if we can't read, we fall back to DEFAULT_PROXY_PORT */ }
1139
+ catch { /* non-fatal */ }
990
1140
  }
991
1141
  }
992
1142
  // Uninstall from each scope
@@ -1011,7 +1161,13 @@ export const uninstallCommand = new Command('uninstall')
1011
1161
  continue;
1012
1162
  }
1013
1163
  if (isSelectiveUninstall) {
1014
- await runSelectivePhaseForScope({ claudeDir, devflowDir, selectedPlugins, verbose });
1164
+ await runSelectivePhaseForScope({
1165
+ claudeDir,
1166
+ devflowDir,
1167
+ selectedPlugins,
1168
+ verbose,
1169
+ installedPlugins: await resolveInstalledPlugins(devflowDir),
1170
+ });
1015
1171
  }
1016
1172
  else {
1017
1173
  await runFullPhaseForScope({ scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs: !!options.keepDocs, isTTY: !!process.stdin.isTTY });
@@ -1151,8 +1307,8 @@ export async function sweepDevflowNamespaces(claudeDir, verbose) {
1151
1307
  * registry (retired agents/commands survive indefinitely without the sweep).
1152
1308
  * For skills: only remove skills that are NOT used by any remaining plugin.
1153
1309
  */
1154
- export async function removeSelectedPlugins(claudeDir, plugins, verbose) {
1155
- const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, DEVFLOW_PLUGINS);
1310
+ export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS) {
1311
+ const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
1156
1312
  const commandsDir = path.join(claudeDir, 'commands', 'devflow');
1157
1313
  for (const cmd of commands) {
1158
1314
  const cmdFileName = mdFileName(cmd.replace(/^\//, ''));
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { safeDeleteCommand } from './cli/commands/safe-delete.js';
19
19
  import { proxyCommand } from './cli/commands/proxy.js';
20
20
  import { agentsCommand } from './cli/commands/agents.js';
21
21
  import { complianceCommand } from './cli/commands/compliance.js';
22
+ import { trackerCommand } from './cli/commands/tracker.js';
22
23
  const __filename = fileURLToPath(import.meta.url);
23
24
  const __dirname = dirname(__filename);
24
25
  // Read version from package.json
@@ -47,6 +48,7 @@ program.addCommand(safeDeleteCommand);
47
48
  program.addCommand(proxyCommand);
48
49
  program.addCommand(agentsCommand);
49
50
  program.addCommand(complianceCommand);
51
+ program.addCommand(trackerCommand);
50
52
  // Handle no command (bare `devflow`) or unknown subcommand.
51
53
  // When Commander sees an unrecognised first argument it does not route to any
52
54
  // registered subcommand; instead the root action fires with that argument
@@ -17,17 +17,35 @@ Run a proactive bug analysis on the current branch by combining static analysis
17
17
 
18
18
  ### Phase 1: Pre-flight
19
19
 
20
- **Produces:** BRANCH_INFO, PR_DESCRIPTION, COMPLIANCE_SKILL_INSTALLED
20
+ **Produces:** BRANCH_INFO, PR_DESCRIPTION, EVIDENCE_POLICY, ISSUE_REQUIRED, APPLY_CONVENTIONS, REQUIRE_NON_AUTHOR_APPROVAL
21
21
 
22
- **Resolve `COMPLIANCE_SKILL_INSTALLED` once per run:** Check whether `~/.claude/skills/devflow:compliance/SKILL.md` exists (one file-existence check, read-only, silent). Set `COMPLIANCE_SKILL_INSTALLED = true` if the file exists, `false` otherwise.
23
- Reuse this result for every downstream phase.
22
+ **Resolve the evidence policy once per run**, from the repository root, before any step reads the values:
23
+
24
+ ```bash
25
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/resolve-evidence-policy.cjs" 2>/dev/null; echo "exit=$?"
26
+ ```
27
+
28
+ Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error> REF=<branch|none>[ WARN=<remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy>[,…]] ISSUE_REQUIRED=<true|false> APPLY_CONVENTIONS=<true|false> REQUIRE_NON_AUTHOR_APPROVAL=<true|false>` — these fields, in this order, nothing else, where `<branch>` is a branch name such as `main`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `EVIDENCE_POLICY=required SOURCE=error REF=none ISSUE_REQUIRED=true APPLY_CONVENTIONS=true REQUIRE_NON_AUTHOR_APPROVAL=true` instead.
29
+
30
+ Set `EVIDENCE_POLICY`, `ISSUE_REQUIRED`, `APPLY_CONVENTIONS` and `REQUIRE_NON_AUTHOR_APPROVAL` from the accepted line. Pass agents only the three mechanism inputs, never `EVIDENCE_POLICY`. Report `Evidence policy: {EVIDENCE_POLICY} (source: {SOURCE})`, plus any `WARN` tokens as advisory, once in the final report.
31
+
32
+ Render the test-plan block from `/implement`'s evidence file, and from nothing else:
33
+ 1. `branch_slug` is `git branch --show-current` with every `/` replaced by `-`.
34
+ 2. Only when `branch_slug` matches `^[A-Za-z0-9._-]{1,200}$` and the file exists, run (the path double-quoted):
35
+
36
+ ```bash
37
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" render --plan ".devflow/docs/evidence-{branch_slug}.md"; echo "exit=$?"
38
+ ```
39
+
40
+ 3. On `exit=0`, `PR_TEST_PLAN_BLOCK` is its stdout byte for byte without that `exit=` line; in every other case it is `(none)`. Nothing here waits on it: the Git agent pastes it only behind its own check, and only into a PR it creates.
24
41
 
25
42
  Spawn Git agent:
26
43
 
27
44
  ```
28
45
  Agent(subagent_type="Git", run_in_background=false):
29
46
  "OPERATION: ensure-pr-ready
30
- COMPLIANCE: {COMPLIANCE_SKILL_INSTALLED ? "enabled" : "(none)"}
47
+ PR_TEST_PLAN_BLOCK: {PR_TEST_PLAN_BLOCK verbatim, or (none)}
48
+ APPLY_CONVENTIONS: {APPLY_CONVENTIONS}
31
49
  Validate branch, commit if needed, push, create PR if needed.
32
50
  Return: branch, base_branch, branch-slug, PR#"
33
51
  ```