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
@@ -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';
@@ -18,6 +20,7 @@ import { removeContextHook } from './context.js';
18
20
  import { applyProxyTeardownToSettings } from './proxy.js';
19
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');
@@ -972,7 +1099,20 @@ export const uninstallCommand = new Command('uninstall')
972
1099
  }
973
1100
  // === DRY RUN: show plan and exit ===
974
1101
  if (dryRun) {
975
- 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
+ });
976
1116
  p.outro(color.dim('No changes made (dry run)'));
977
1117
  return;
978
1118
  }
@@ -1021,7 +1161,13 @@ export const uninstallCommand = new Command('uninstall')
1021
1161
  continue;
1022
1162
  }
1023
1163
  if (isSelectiveUninstall) {
1024
- await runSelectivePhaseForScope({ claudeDir, devflowDir, selectedPlugins, verbose });
1164
+ await runSelectivePhaseForScope({
1165
+ claudeDir,
1166
+ devflowDir,
1167
+ selectedPlugins,
1168
+ verbose,
1169
+ installedPlugins: await resolveInstalledPlugins(devflowDir),
1170
+ });
1025
1171
  }
1026
1172
  else {
1027
1173
  await runFullPhaseForScope({ scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs: !!options.keepDocs, isTTY: !!process.stdin.isTTY });
@@ -1161,8 +1307,8 @@ export async function sweepDevflowNamespaces(claudeDir, verbose) {
1161
1307
  * registry (retired agents/commands survive indefinitely without the sweep).
1162
1308
  * For skills: only remove skills that are NOT used by any remaining plugin.
1163
1309
  */
1164
- export async function removeSelectedPlugins(claudeDir, plugins, verbose) {
1165
- 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);
1166
1312
  const commandsDir = path.join(claudeDir, 'commands', 'devflow');
1167
1313
  for (const cmd of commands) {
1168
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
  ```
@@ -37,6 +37,20 @@ Run a comprehensive code review of the current branch by spawning parallel revie
37
37
  **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.
38
38
  Reuse this result for every worktree and every downstream phase.
39
39
 
40
+ #### Step 0b-ii: Resolve the evidence policy
41
+
42
+ **Produces:** EVIDENCE_POLICY, ISSUE_REQUIRED, APPLY_CONVENTIONS, REQUIRE_NON_AUTHOR_APPROVAL
43
+
44
+ **Resolve the evidence policy once per run**, from the repository root, before any step reads the values:
45
+
46
+ ```bash
47
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/resolve-evidence-policy.cjs" 2>/dev/null; echo "exit=$?"
48
+ ```
49
+
50
+ 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.
51
+
52
+ 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.
53
+
40
54
  #### Step 0c: Per-Worktree Pre-Flight (Git Agent)
41
55
 
42
56
  **Produces:** BRANCH_INFO, PR_INFO, PR_DESCRIPTION, PR_DESCRIPTION_GUIDANCE
@@ -48,6 +62,16 @@ Discover PR description guidance from plan artifact (per worktree):
48
62
  3. Read the most recent file, extract `## PR Description Guidance` section
49
63
  4. If no plan files exist or section not found, set `PR_DESCRIPTION_GUIDANCE` to `(none)`
50
64
 
65
+ Render the test-plan block (per worktree) from `/implement`'s evidence file, and from nothing else:
66
+ 1. `branch_slug` is `git -C "{worktree}" branch --show-current` with every `/` replaced by `-`.
67
+ 2. Only when `branch_slug` matches `^[A-Za-z0-9._-]{1,200}$` and the file exists, run (the path double-quoted):
68
+
69
+ ```bash
70
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" render --plan "{worktree}/.devflow/docs/evidence-{branch_slug}.md"; echo "exit=$?"
71
+ ```
72
+
73
+ 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.
74
+
51
75
  For each reviewable worktree, spawn Git agent:
52
76
 
53
77
  ```
@@ -55,7 +79,8 @@ Agent(subagent_type="Git", run_in_background=false):
55
79
  "OPERATION: ensure-pr-ready
56
80
  WORKTREE_PATH: {worktree_path} (omit if cwd)
57
81
  PR_DESCRIPTION_GUIDANCE: {pr_description_guidance}
58
- COMPLIANCE: {COMPLIANCE_SKILL_INSTALLED ? "enabled" : "(none)"}
82
+ PR_TEST_PLAN_BLOCK: {PR_TEST_PLAN_BLOCK verbatim, or (none)}
83
+ APPLY_CONVENTIONS: {APPLY_CONVENTIONS}
59
84
  Validate branch, commit if needed, push, create PR if needed.
60
85
  Return: branch, base_branch, branch-slug, PR#"
61
86
  ```
@@ -133,13 +158,15 @@ MAX_REVIEW_CYCLES = 10
133
158
  #### Step 0f: Resolve Publication Mode (Per Worktree)
134
159
 
135
160
  **Produces:** REVIEW_PUBLICATION
136
- **Requires:** WORKTREES
161
+ **Requires:** WORKTREES, EVIDENCE_POLICY
137
162
 
138
163
  For each reviewable worktree, call:
139
164
 
140
165
  **Resolve `REVIEW_PUBLICATION` per worktree:** Read the current worktree's `.devflow/config.json` (a single, direct file read — multi-worktree repos may have different publication settings per worktree root). If the file exists and `reviewPublication` is one of `auto`, `full`, or `off`, set `REVIEW_PUBLICATION` to that value; otherwise set `REVIEW_PUBLICATION = "auto"`.
141
166
 
142
- Note: `auto` is NOT fail-open — under `auto`, the Git agent probes the repository visibility and treats any error or unrecognised value as PUBLIC (mode STUB).
167
+ **Evidence stub:** only when `EVIDENCE_POLICY` is `required`, a resolved `off` becomes `stub`, so a counts-only record still reaches the PR. `stub` is never a config value: a configured `stub` is unrecognised and resolves to `auto` like any other.
168
+
169
+ Note: `auto` is NOT fail-open — under `auto`, the Git agent probes the repository visibility and treats any error or unrecognised value as PUBLIC (mode STUB). What each value does is decided by the Git agent's publication gate (`references/publication-gate.md` step 2); this partial only resolves the value.
143
170
 
144
171
  ### Phase 1: Analyze Changed Files
145
172
 
@@ -165,6 +192,8 @@ Per worktree, detect file types in diff using `DIFF_RANGE` to determine conditio
165
192
 
166
193
  If `COMPLIANCE_SKILL_INSTALLED` AND the diff touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention): add `compliance` to REVIEW_FOCUS_LIST for this worktree.
167
194
 
195
+ **Language focus presence gate.** The eight language focuses — `typescript`, `react`, `accessibility`, `ui-design`, `go`, `java`, `python`, `rust` — ship with optional plugins, so their pattern skills are installed only when the user selected that plugin. Gate them exactly as `compliance` is gated: for each language focus the table above would add, check whether `~/.claude/skills/devflow:{focus}/SKILL.md` exists (one file-existence check per candidate focus, read-only, silent). If it does not exist, do NOT add that focus to `REVIEW_FOCUS_LIST` and do NOT spawn a Review agent for it — the file-type condition alone never spawns a language focus. The eight core focuses are unconditional and are never presence-gated.
196
+
168
197
  ### Phase 1b: Load Decisions Index
169
198
 
170
199
  **Produces:** DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, COMPLIANCE_SKILL_INSTALLED (carried from Step 0b)
@@ -248,14 +277,14 @@ Spawn Review agents **in a single message**. Always run 8 core reviews; conditio
248
277
  | regression | ✓ | devflow:regression |
249
278
  | testing | ✓ | devflow:testing |
250
279
  | reliability | ✓ | devflow:reliability |
251
- | typescript | conditional | devflow:typescript |
252
- | react | conditional | devflow:react |
253
- | accessibility | conditional | devflow:accessibility |
254
- | ui-design | conditional | devflow:ui-design |
255
- | go | conditional | devflow:go |
256
- | java | conditional | devflow:java |
257
- | python | conditional | devflow:python |
258
- | rust | conditional | devflow:rust |
280
+ | typescript | presence-gated | devflow:typescript |
281
+ | react | presence-gated | devflow:react |
282
+ | accessibility | presence-gated | devflow:accessibility |
283
+ | ui-design | presence-gated | devflow:ui-design |
284
+ | go | presence-gated | devflow:go |
285
+ | java | presence-gated | devflow:java |
286
+ | python | presence-gated | devflow:python |
287
+ | rust | presence-gated | devflow:rust |
259
288
  | database | conditional | devflow:database |
260
289
  | dependencies | conditional | devflow:dependencies |
261
290
  | documentation | conditional | devflow:documentation |
@@ -309,7 +338,7 @@ Output: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/review-s
309
338
  Agent(subagent_type="Git", run_in_background=false):
310
339
  "OPERATION: post-review-summary
311
340
  PR_NUMBER: {pr_number}
312
- REVIEW_SUMMARY_PATH: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/review-summary.md
341
+ REVIEW_SUMMARY_PATH: .devflow/docs/reviews/{branch-slug}/{timestamp}/review-summary.md
313
342
  CYCLE_NUMBER: {cycle_number}
314
343
  REVIEW_TIMESTAMP: {timestamp}
315
344
  REVIEW_PUBLICATION: {REVIEW_PUBLICATION}
@@ -327,7 +356,7 @@ Per worktree, after successful completion:
327
356
  - Merge recommendation (from Synthesize agent)
328
357
  - Issue counts by category (🔴 blocking / ⚠️ should-fix / ℹ️ pre-existing)
329
358
  - Review comment status: POSTED / POSTED+TRUNCATED (body exceeded 60k after redaction) / SKIPPED (D7 dedup) / DEGRADED (from Git)
330
- - Publication status: FULL (private repo) | FULL (config override) | STUB (public repository) | OFF (publication disabled by config) (from Git agent)
359
+ - Publication status: FULL (private repo) | FULL (config override) | STUB (public repository) | OFF (publication disabled by config) | STUB (visibility undeterminable) | STUB (evidence policy) (from Git agent)
331
360
  - Artifact paths
332
361
 
333
362
  In multi-worktree mode, report results per worktree.
@@ -378,14 +407,14 @@ In multi-worktree mode, report results per worktree.
378
407
  | Worktree pre-flight fails | Report failure, continue with other worktrees |
379
408
  | `--full` in multi-worktree mode | Applies to all worktrees (global modifier) |
380
409
  | Many worktrees (5+) | Report count and proceed — user manages their worktree count |
381
- | Review comment already posted | Git agent matches `<!-- devflow:review-summary cycle:{N} ts:{REVIEW_TIMESTAMP}` pair → skip silently (D7); a re-review in the same cycle (different timestamp) posts its own comment |
410
+ | Review comment already posted | Git agent matches its own marker on the `{cycle, timestamp}` pair → skip silently (D7); a re-review in the same cycle (different timestamp) posts its own comment. The marker's format belongs to the operation; this caller passes the pair and never restates the literal |
382
411
  | First review (no prior resolution) | PRIOR_RESOLUTIONS=(none), no convergence check |
383
412
  | fp_ratio denominator = 0 | fp_ratio = 0, no warning |
384
413
  | `--full` flag | Bypass incremental detection (Step 0d), still load PRIOR_RESOLUTIONS for cross-cycle awareness |
385
414
  | Parsing failure on resolution-summary.md | fp_ratio = 0, convergence tracking degraded (see Step 0e-ii) |
386
415
  | Concurrent sessions | Advisory only, each session computes independently |
387
416
  | Public repo (`auto` mode) | Git agent probes visibility, posts counts-only STUB comment; full report stays local |
388
- | `reviewPublication: off` | Git agent skips posting the review comment (OFF) |
417
+ | `reviewPublication: off` | Git agent skips posting the review comment (OFF) — except that, only when `EVIDENCE_POLICY` is `required`, Step 0f passes `stub` and a counts-only STUB comment posts |
389
418
 
390
419
  ## Backwards Compatibility
391
420