devflow-kit 3.8.0 → 3.10.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 (211) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +6 -6
  3. package/dist/agents/code.md +10 -10
  4. package/dist/agents/fix.md +7 -7
  5. package/dist/agents/git.md +2 -2
  6. package/dist/agents/harden.md +7 -6
  7. package/dist/agents/refactor.md +6 -6
  8. package/dist/agents/skim.md +1 -1
  9. package/dist/agents/triage.md +7 -0
  10. package/dist/cli/commands/ambient.js +10 -2
  11. package/dist/cli/commands/compliance-prompts.js +9 -4
  12. package/dist/cli/commands/compliance.js +171 -24
  13. package/dist/cli/commands/curation.js +79 -23
  14. package/dist/cli/commands/gate-level-prompts.js +39 -23
  15. package/dist/cli/commands/gates.js +6 -7
  16. package/dist/cli/commands/init.js +150 -145
  17. package/dist/cli/commands/install-report.js +34 -0
  18. package/dist/cli/commands/legacy-sweep.js +64 -0
  19. package/dist/cli/commands/memory.js +59 -12
  20. package/dist/cli/commands/proxy.js +6 -1
  21. package/dist/cli/commands/rules.js +29 -12
  22. package/dist/cli/commands/skills.js +42 -9
  23. package/dist/cli/commands/tracker.js +9 -3
  24. package/dist/cli/commands/uninstall.js +354 -60
  25. package/dist/commands/bug-analysis.md +71 -122
  26. package/dist/commands/code-review.md +11 -11
  27. package/dist/commands/debug.md +2 -2
  28. package/dist/commands/dynamic-build.md +25 -25
  29. package/dist/commands/dynamic-plan.md +2 -2
  30. package/dist/commands/dynamic-profile.md +2 -2
  31. package/dist/commands/dynamic-tickets.md +2 -2
  32. package/dist/commands/explore.md +2 -2
  33. package/dist/commands/implement.md +48 -34
  34. package/dist/commands/plan.md +2 -2
  35. package/dist/commands/release.md +3 -3
  36. package/dist/commands/research.md +2 -2
  37. package/dist/commands/resolve.md +50 -46
  38. package/dist/commands/self-review.md +10 -9
  39. package/dist/compliance-off/agents/git.md +120 -0
  40. package/dist/compliance-off/agents/triage.md +180 -0
  41. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  42. package/dist/compliance-off/commands/code-review.md +520 -0
  43. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  44. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  45. package/dist/compliance-off/commands/implement.md +736 -0
  46. package/dist/compliance-off/commands/plan.md +671 -0
  47. package/dist/compliance-off/commands/release.md +309 -0
  48. package/dist/compliance-off/commands/resolve.md +742 -0
  49. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  50. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  51. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  52. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  53. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  54. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  55. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  56. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  57. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  58. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  59. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  60. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  61. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  62. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  63. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  64. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  65. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  66. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  67. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  68. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  69. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  70. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  71. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  72. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  73. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  74. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  75. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  76. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  77. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  78. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  79. package/dist/core/agent-models.js +19 -4
  80. package/dist/core/area-pause-notice.js +44 -0
  81. package/dist/core/assets.js +126 -17
  82. package/dist/core/compliance-off-files.js +27 -0
  83. package/dist/core/curation-move-skip.js +4 -5
  84. package/dist/core/curation-queue-cleanup.js +6 -5
  85. package/dist/core/curation-store.js +1 -0
  86. package/dist/core/curation-variants.js +460 -112
  87. package/dist/core/evidence-policy.js +10 -8
  88. package/dist/core/flags.js +2 -2
  89. package/dist/core/installed-variants.js +63 -0
  90. package/dist/core/manifest.js +48 -5
  91. package/dist/core/mds-variants.js +700 -3
  92. package/dist/core/orphan-sweep.js +11 -2
  93. package/dist/core/plugins.js +69 -12
  94. package/dist/core/tracked-areas.js +138 -14
  95. package/dist/curation-compliance-off/agents/triage.md +176 -0
  96. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  97. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  98. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  99. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  100. package/dist/curation-compliance-off/commands/implement.md +689 -0
  101. package/dist/curation-compliance-off/commands/plan.md +631 -0
  102. package/dist/curation-compliance-off/commands/release.md +279 -0
  103. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  104. package/dist/curation-off/agents/code.md +10 -10
  105. package/dist/curation-off/agents/fix.md +7 -7
  106. package/dist/curation-off/agents/harden.md +7 -6
  107. package/dist/curation-off/agents/refactor.md +6 -6
  108. package/dist/curation-off/agents/triage.md +7 -0
  109. package/dist/curation-off/commands/bug-analysis.md +69 -119
  110. package/dist/curation-off/commands/code-review.md +11 -11
  111. package/dist/curation-off/commands/debug.md +2 -2
  112. package/dist/curation-off/commands/dynamic-build.md +25 -25
  113. package/dist/curation-off/commands/explore.md +2 -2
  114. package/dist/curation-off/commands/implement.md +48 -34
  115. package/dist/curation-off/commands/plan.md +2 -2
  116. package/dist/curation-off/commands/release.md +1 -1
  117. package/dist/curation-off/commands/resolve.md +50 -46
  118. package/dist/curation-off/commands/self-review.md +10 -9
  119. package/dist/hud/components/curation-counts.js +27 -0
  120. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  121. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  122. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  123. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  124. package/dist/skills/git/references/publication-gate.md +6 -7
  125. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  126. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  127. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  128. package/dist/targets/claude-code/behavior-install.js +23 -9
  129. package/dist/targets/claude-code/charter-render.js +374 -0
  130. package/dist/targets/claude-code/compliance-install.js +32 -12
  131. package/dist/targets/claude-code/compliance-off-files.js +245 -0
  132. package/dist/targets/claude-code/curation-install.js +570 -100
  133. package/dist/targets/claude-code/installer.js +467 -62
  134. package/dist/targets/claude-code/language-stamp.js +6 -0
  135. package/dist/targets/claude-code/linked-folder.js +182 -0
  136. package/dist/targets/claude-code/reference-converge.js +118 -0
  137. package/dist/targets/claude-code/tracker-install.js +21 -8
  138. package/dist/targets/claude-code/tracker-servers.js +20 -6
  139. package/dist/targets/claude-code/workflow-folder-install.js +231 -0
  140. package/dist/variant-axes.json +109 -0
  141. package/package.json +1 -1
  142. package/src/assets/agents/_partials/_code.mds +6 -6
  143. package/src/assets/agents/code.mds +4 -4
  144. package/src/assets/agents/evaluate.md +7 -13
  145. package/src/assets/agents/fix.mds +1 -1
  146. package/src/assets/agents/git.mds +34 -2
  147. package/src/assets/agents/harden.mds +7 -6
  148. package/src/assets/agents/skim.mds +1 -1
  149. package/src/assets/agents/test.md +21 -12
  150. package/src/assets/agents/triage.mds +9 -0
  151. package/src/assets/agents/validate.md +27 -34
  152. package/src/assets/commands/_partials/_compliance.mds +5 -3
  153. package/src/assets/commands/_partials/_decisions.mds +10 -8
  154. package/src/assets/commands/_partials/_engine.mds +9 -8
  155. package/src/assets/commands/_partials/_publication.mds +10 -1
  156. package/src/assets/commands/_partials/_settings.mds +2 -2
  157. package/src/assets/commands/bug-analysis.mds +80 -126
  158. package/src/assets/commands/code-review.mds +52 -9
  159. package/src/assets/commands/dynamic-build.mds +109 -15
  160. package/src/assets/commands/dynamic-tickets.mds +13 -0
  161. package/src/assets/commands/implement.mds +145 -33
  162. package/src/assets/commands/plan.mds +19 -0
  163. package/src/assets/commands/release.mds +50 -1
  164. package/src/assets/commands/resolve.mds +229 -44
  165. package/src/assets/commands/self-review.mds +8 -7
  166. package/src/assets/mds/git/_ops.mds +29 -1
  167. package/src/assets/mds/git/_ops_pr.mds +20 -10
  168. package/src/assets/mds/git/_ops_steps.mds +27 -0
  169. package/src/assets/mds/git/_pr.mds +15 -15
  170. package/src/assets/mds/git/_references.mds +12 -7
  171. package/src/assets/mds/tracker/_common.mds +4 -0
  172. package/src/assets/mds/tracker/_github.mds +43 -4
  173. package/src/assets/mds/tracker/_jira.mds +44 -4
  174. package/src/assets/mds/tracker/_linear.mds +44 -4
  175. package/src/assets/rules/quality.md +2 -0
  176. package/src/assets/scripts/ci-wait.cjs +493 -118
  177. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  178. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  179. package/src/assets/scripts/hooks/background-memory-update +79 -1
  180. package/src/assets/scripts/hooks/json-helper.cjs +44 -2
  181. package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
  182. package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
  183. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
  184. package/src/assets/scripts/hooks/memory-permission +10 -2
  185. package/src/assets/scripts/hooks/memory-worker +11 -0
  186. package/src/assets/scripts/hooks/pre-compact-memory +11 -0
  187. package/src/assets/scripts/hooks/preamble +18 -8
  188. package/src/assets/scripts/hooks/queue-append +218 -1
  189. package/src/assets/scripts/hooks/session-start-context +89 -37
  190. package/src/assets/scripts/hooks/session-start-memory +39 -0
  191. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  192. package/src/assets/scripts/pr-evidence.cjs +46 -4
  193. package/src/assets/scripts/resolve-settings.cjs +244 -30
  194. package/src/assets/scripts/verify-evidence.cjs +708 -124
  195. package/src/assets/skills/agent-references/SKILL.md +7 -1
  196. package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
  197. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  198. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  199. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
  200. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  201. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  202. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  203. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  204. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  205. package/src/assets/skills/git/SKILL.md +3 -3
  206. package/src/assets/skills/git/references/github-api.md +2 -139
  207. package/src/assets/skills/git/references/github-posting.md +158 -0
  208. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  209. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  210. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  211. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -8,8 +8,11 @@ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
8
8
  import { discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
9
9
  import { getGitRoot } from '../../core/git.js';
10
10
  import { firstSymbolicLink } from '../../core/linked-path.js';
11
+ import { pooled } from '../../core/pool.js';
11
12
  import { getMemoryDir, getPendingTurnsPath, getPendingTurnsProcessingPath, } from '../../core/project-paths.js';
12
13
  import { drainQueueFiles, formatRefusedDrain } from '../../core/queue-drain.js';
14
+ import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
15
+ import { probeTrackedArea } from '../../core/tracked-areas.js';
13
16
  import { HOOKS_DIR_SUFFIX, devflowHookOwner, endsWithAny, ensureHook, hasHook, removeHooks, runHookCommand, runHookSuffix, } from '../../targets/claude-code/hooks.js';
14
17
  import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
15
18
  import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
@@ -159,21 +162,42 @@ export function convergeMemoryHooks(settingsJson, enabled, devflowDir) {
159
162
  const cleaned = removeMemoryHooks(settingsJson);
160
163
  return enabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
161
164
  }
165
+ /**
166
+ * Why the memory area at `projectRoot` is paused, or null when git tracks nothing in the
167
+ * memory folder there. The area is paused when git tracks files in the memory folder, or
168
+ * a `.git` entry stands and git did not answer (`unknown`); a root with no `.git` entry
169
+ * makes no git call. The root is the checkout's toplevel, where the memory hooks work.
170
+ */
171
+ async function memoryPauseReason(projectRoot) {
172
+ const { answer } = await probeTrackedArea(projectRoot, 'memory');
173
+ return answer === 'untracked' ? null : answer;
174
+ }
162
175
  /**
163
176
  * Drain a project's pending memory queue (and a claimed batch) so stale turns
164
177
  * are not processed when memory is next switched on. Shared by `devflow init
165
- * --no-memory` and `devflow memory --disable`. Refused, deleting nothing, when
166
- * `.devflow` or `.devflow/memory` under `projectRoot` is a symbolic link
167
- * (D-CLI-NO-SYMLINK). ENOENT-tolerant; any other error propagates to the command
168
- * boundary, like drainCurationQueue.
178
+ * --no-memory` and `devflow memory --disable`. Deletes nothing, and answers
179
+ * `skipped`, when git tracks files in the memory folder or a `.git` entry stands and
180
+ * git does not answer: a folder that git tracks is the repository's content. Refused,
181
+ * deleting nothing, when `.devflow` or `.devflow/memory` under `projectRoot` is a
182
+ * symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; any other error propagates to the
183
+ * command boundary, like drainCurationQueue.
169
184
  */
170
185
  export async function drainMemoryQueue(projectRoot) {
186
+ const skipped = await memoryPauseReason(projectRoot);
187
+ if (skipped !== null)
188
+ return { drained: false, skipped };
171
189
  const memoryDir = getMemoryDir(projectRoot);
172
190
  return drainQueueFiles([path.dirname(memoryDir), memoryDir], [
173
191
  getPendingTurnsPath(projectRoot),
174
192
  getPendingTurnsProcessingPath(projectRoot),
175
193
  ]);
176
194
  }
195
+ /** The line a caller prints for a memory drain that deleted nothing: the fixed notice of a paused area, or the refusal at a link. */
196
+ export function formatMemoryNotDrained(drain) {
197
+ if (!('skipped' in drain))
198
+ return formatRefusedDrain('memory', drain.linkedFolder);
199
+ return `The memory queue was not drained. ${AREA_PAUSE_NOTICE.memory[drain.skipped]}`;
200
+ }
177
201
  /**
178
202
  * Returns true if the given project root contains a `.devflow/memory/` directory.
179
203
  * Treats unexpected errors (e.g. EACCES) as absent to avoid false positives.
@@ -200,15 +224,23 @@ export async function filterProjectsWithMemory(gitRoots) {
200
224
  const checks = await Promise.all(gitRoots.map(async (root) => ({ root, has: await hasMemoryDir(root) })));
201
225
  return checks.filter((c) => c.has).map((c) => c.root);
202
226
  }
227
+ /** At most this many projects are checked and cleaned at once, so `memory --clear` starts few git processes. */
228
+ export const CLEAN_QUEUE_POOL_LIMIT = 8;
203
229
  /**
204
230
  * Clean up memory queue files from the given project paths.
205
231
  * Skips projects where the background updater lock is held to avoid data loss,
206
- * and refuses, deleting nothing there, a project whose `.devflow` or
207
- * `.devflow/memory` is a symbolic link (D-CLI-NO-SYMLINK); `refused` names each
208
- * such link. Returns the count of projects from which at least one file was removed.
232
+ * leaves a project whose memory area is paused alone (git tracks files in the memory
233
+ * folder, or a `.git` entry stands and git did not answer; `paused` names each such
234
+ * project with the reason), and refuses, deleting nothing there, a project whose
235
+ * `.devflow` or `.devflow/memory` is a symbolic link (D-CLI-NO-SYMLINK); `refused` names
236
+ * each such link. At most {@link CLEAN_QUEUE_POOL_LIMIT} projects are handled at once.
237
+ * Returns the count of projects from which at least one file was removed.
209
238
  */
210
239
  export async function cleanQueueFiles(projectPaths) {
211
- const results = await Promise.all(projectPaths.map(async (project) => {
240
+ const settled = await pooled(projectPaths, CLEAN_QUEUE_POOL_LIMIT, async (project) => {
241
+ const reason = await memoryPauseReason(project);
242
+ if (reason !== null)
243
+ return { paused: reason, project };
212
244
  const memDir = getMemoryDir(project);
213
245
  let linkedFolder;
214
246
  try {
@@ -234,10 +266,16 @@ export async function cleanQueueFiles(projectPaths) {
234
266
  fs.unlink(getPendingTurnsProcessingPath(project)).then(() => true).catch(() => false),
235
267
  ]);
236
268
  return (q || pr) ? { cleaned: project } : null;
237
- }));
269
+ });
270
+ const results = settled.map((entry) => {
271
+ if (entry.status === 'rejected')
272
+ throw entry.reason;
273
+ return entry.value;
274
+ });
238
275
  const cleanedProjects = results.flatMap((r) => (r !== null && 'cleaned' in r ? [r.cleaned] : []));
239
276
  const refused = results.flatMap((r) => (r !== null && 'refused' in r ? [r.refused] : []));
240
- return { cleaned: cleanedProjects.length, projects: cleanedProjects, refused };
277
+ const paused = results.flatMap((r) => (r !== null && 'paused' in r ? [{ project: r.project, reason: r.paused }] : []));
278
+ return { cleaned: cleanedProjects.length, projects: cleanedProjects, refused, paused };
241
279
  }
242
280
  export const memoryCommand = new Command('memory')
243
281
  .description('Enable, disable, or clean up working memory (session context preservation)')
@@ -297,10 +335,13 @@ export const memoryCommand = new Command('memory')
297
335
  }
298
336
  targets = scope === 'local' && currentProject ? [currentProject] : allProjects;
299
337
  }
300
- const { cleaned, projects: cleanedProjects, refused } = await cleanQueueFiles(targets);
338
+ const { cleaned, projects: cleanedProjects, refused, paused } = await cleanQueueFiles(targets);
301
339
  for (const project of cleanedProjects) {
302
340
  p.log.info(color.dim(`Cleaned: ${project}`));
303
341
  }
342
+ for (const { project, reason } of paused) {
343
+ p.log.warn(`The memory queue of ${project} was not cleaned. ${AREA_PAUSE_NOTICE.memory[reason]}`);
344
+ }
304
345
  for (const linkedFolder of refused) {
305
346
  p.log.warn(formatRefusedDrain('memory', linkedFolder));
306
347
  }
@@ -340,6 +381,12 @@ export const memoryCommand = new Command('memory')
340
381
  const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'memory') : null;
341
382
  if (narrowed !== null)
342
383
  p.log.info(`Effective here: ${color.yellow(narrowed)}`);
384
+ // A paused memory area (git tracks files in the memory folder, or did not answer):
385
+ // the fixed notice, from the checkout's toplevel, where the memory hooks work. The
386
+ // current directory stands in outside a git work tree, as it does for the settings line.
387
+ const paused = await memoryPauseReason((await getGitRoot()) ?? process.cwd());
388
+ if (paused !== null)
389
+ p.log.info(AREA_PAUSE_NOTICE.memory[paused]);
343
390
  const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
344
391
  if (trackedWarning !== null)
345
392
  p.log.warn(trackedWarning);
@@ -380,7 +427,7 @@ export const memoryCommand = new Command('memory')
380
427
  if (gitRoot) {
381
428
  const drain = await drainMemoryQueue(gitRoot);
382
429
  if (!drain.drained)
383
- p.log.warn(formatRefusedDrain('memory', drain.linkedFolder));
430
+ p.log.warn(formatMemoryNotDrained(drain));
384
431
  }
385
432
  p.log.success('Working memory disabled in every project');
386
433
  });
@@ -31,6 +31,7 @@ import { stripAnsi } from '../../hud/colors.js';
31
31
  import { reapplyAgentMapping, revertExternalAgents, countExternalMappedAgents, readAgentMapping, } from '../../core/agent-models.js';
32
32
  import { getExternalModelsCached, discoverExternalModels, } from '../../core/model-discovery.js';
33
33
  import { modelCacheDir } from '../../core/cache.js';
34
+ import { formatLinksSummary } from './install-report.js';
34
35
  import { getClaudeDirectory, getDevFlowDirectory, getHomeDirectory, } from '../../targets/claude-code/claude-paths.js';
35
36
  import { devflowHookOwner, ensureHook, hasHook, removeHooks, runHookCommand, } from '../../targets/claude-code/hooks.js';
36
37
  function Ok(value) {
@@ -1389,6 +1390,8 @@ async function runEnable(portOption) {
1389
1390
  s.message('Warming model cache...');
1390
1391
  await discoverExternalModels(cacheDir, logPath).catch(() => { });
1391
1392
  s.stop(color.green('External model routing enabled'));
1393
+ for (const { message } of formatLinksSummary(reapplyResult))
1394
+ p.log.warn(message);
1392
1395
  // D-P4-1: applies-on-restart — env var takes effect only for new sessions
1393
1396
  p.log.info(color.dim('Context-window enforcement disabled for relay-routed models — applies to new Claude Code sessions'));
1394
1397
  if (adopted) {
@@ -1472,7 +1475,9 @@ async function runDisable() {
1472
1475
  // Step 3: Sync manifest
1473
1476
  await syncManifestFeature(devflowDir, 'proxy', false);
1474
1477
  // Step 4: Revert external agents to shipped defaults
1475
- await revertExternalAgents({ installDir, devflowDir });
1478
+ const revertResult = await revertExternalAgents({ installDir, devflowDir });
1479
+ for (const { message } of formatLinksSummary(revertResult))
1480
+ p.log.warn(message);
1476
1481
  p.log.success('External model routing disabled');
1477
1482
  // Step 5: Terminate the relay process.
1478
1483
  // Identity is confirmed via TCP + health before signalling — a recycled PID
@@ -10,6 +10,8 @@ import { rulesDir } from '../../core/assets.js';
10
10
  import { installAllRules, validateRuleShadow } from '../../targets/claude-code/installer.js';
11
11
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
12
12
  import { convergeBehaviorFromManifest } from '../../targets/claude-code/behavior-install.js';
13
+ import { newLinkReport, refuseUnrealFolder, refusedFolderMessage, removeReportingLink } from '../../targets/claude-code/linked-folder.js';
14
+ import { formatLinksSummary } from './install-report.js';
13
15
  /**
14
16
  * Check whether a rule has a user shadow file at ~/.devflow/rules/{name}.md.
15
17
  */
@@ -213,17 +215,24 @@ export async function runRulesEnable(claudeDir, devflowDir) {
213
215
  }
214
216
  const installedPlugins = DEVFLOW_PLUGINS.filter(pl => manifest.plugins.includes(pl.name));
215
217
  const rulesMap = buildRulesMap(installedPlugins);
216
- // Wipe stale rules from previously uninstalled plugins before re-installing.
217
- await fs.rm(rulesTarget, { recursive: true, force: true });
218
- await fs.mkdir(rulesTarget, { recursive: true });
218
+ // Wipe stale rules from previously uninstalled plugins before re-installing. A rules folder that is
219
+ // a link is unlinked and replaced by a real folder, never emptied through, and the link is reported.
220
+ const links = newLinkReport();
221
+ links.replacedLinks.push(...await removeReportingLink(rulesTarget));
222
+ // A link that could not be removed is still there, and `mkdir` and every rule write would follow it.
223
+ // Prove the folder first; a refused folder gets no rule, no manifest write and one refusal line.
224
+ const rulesFolderRefused = await refuseUnrealFolder(links, rulesTarget);
219
225
  let outcomes = [];
220
- let installFailed = false;
221
- try {
222
- outcomes = await installAllRules(rulesMap, devflowDir, rulesTarget);
223
- }
224
- catch (err) {
225
- p.log.error(`Rules installation failed — rules directory has been cleared: ${err instanceof Error ? err.message : String(err)}`);
226
- installFailed = true;
226
+ let installFailed = rulesFolderRefused;
227
+ if (!rulesFolderRefused) {
228
+ await fs.mkdir(rulesTarget, { recursive: true });
229
+ try {
230
+ outcomes = await installAllRules(rulesMap, devflowDir, rulesTarget, links);
231
+ }
232
+ catch (err) {
233
+ p.log.error(`Rules installation failed — rules directory has been cleared: ${err instanceof Error ? err.message : String(err)}`);
234
+ installFailed = true;
235
+ }
227
236
  }
228
237
  if (!installFailed) {
229
238
  const shadowedCount = outcomes.filter(o => o.outcome === 'shadow').length;
@@ -239,6 +248,14 @@ export async function runRulesEnable(claudeDir, devflowDir) {
239
248
  await writeManifest(devflowDir, manifest);
240
249
  p.log.success(`Installed ${rulesMap.size} rule(s) to ${color.dim(rulesTarget)}${shadowSuffix}`);
241
250
  }
251
+ for (const { message } of formatLinksSummary(links))
252
+ p.log.warn(message);
253
+ // Each converge below proves the rules folder again and names a refused one itself; the line above
254
+ // has already named it, so the repeat is dropped.
255
+ const warnConverge = (msg) => {
256
+ if (!(rulesFolderRefused && msg === refusedFolderMessage(rulesTarget)))
257
+ p.log.warn(msg);
258
+ };
242
259
  // Converge compliance artifacts unconditionally — convergeFromManifest handles the
243
260
  // disabled case itself (removes stale artifacts). Running this regardless of installFailed
244
261
  // prevents a rules-install failure from leaving the compliance rule in a stale state.
@@ -249,7 +266,7 @@ export async function runRulesEnable(claudeDir, devflowDir) {
249
266
  claudeDir,
250
267
  devflowDir,
251
268
  manifest,
252
- warn: (msg) => p.log.warn(msg),
269
+ warn: warnConverge,
253
270
  rulesEnabledOverride: !installFailed,
254
271
  });
255
272
  }
@@ -265,7 +282,7 @@ export async function runRulesEnable(claudeDir, devflowDir) {
265
282
  claudeDir,
266
283
  devflowDir,
267
284
  manifest,
268
- warn: (msg) => p.log.warn(msg),
285
+ warn: warnConverge,
269
286
  rulesEnabledOverride: !installFailed,
270
287
  });
271
288
  }
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
7
- import { getAllSkillNames, prefixSkillName, unprefixSkillName, skillOwners, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
7
+ import { getAllSkillNames, prefixSkillName, unprefixSkillName, skillOwners, FEATURE_OWNED_SKILLS, WORKFLOW_FOLDER_NAMES } from '../../core/plugins.js';
8
8
  import { skillsDir } from '../../core/assets.js';
9
9
  import { copyDirectory, validateSkillShadow } from '../../targets/claude-code/installer.js';
10
10
  /**
@@ -42,6 +42,22 @@ export function shadowSeedDir(bareName, installedDir) {
42
42
  return sourceDir;
43
43
  return installedDir ?? sourceDir;
44
44
  }
45
+ /**
46
+ * D-WORKFLOW-SHADOW-REFUSAL: why `devflow skills shadow <name>` refuses a workflow that ships as a
47
+ * skill folder, or null for any other name.
48
+ *
49
+ * A folder installs from the build tree of the machine's exact variant key, with no source
50
+ * directory to copy and no shadow to overlay, so a shadow of its name could never apply. Refusing
51
+ * at the command says so before the user edits a copy that does nothing; the install ignores a
52
+ * shadow that is already there, with a warning, and keeps it.
53
+ *
54
+ * @param folderWorkflows - The target-layer set; the default is the real set.
55
+ */
56
+ export function workflowFolderShadowRefusal(bareName, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
57
+ if (!folderWorkflows.has(bareName))
58
+ return null;
59
+ return `${bareName} ships as a skill folder built for the machine's variant, so a shadow cannot replace it. Nothing was shadowed.`;
60
+ }
45
61
  /**
46
62
  * Check if a skill has a shadow (personal override).
47
63
  */
@@ -85,16 +101,19 @@ function buildSkillShadowTag(shadowState) {
85
101
  }
86
102
  }
87
103
  }
88
- export const skillsCommand = new Command('skills')
89
- .description('Manage skill overrides (shadow/unshadow/list)')
90
- .argument('<action>', 'Action: shadow, unshadow, or list')
91
- .argument('[name]', 'Skill name (required for shadow/unshadow)')
92
- .action(async (action, name) => {
104
+ /**
105
+ * Run one `devflow skills` action: shadow, unshadow or list.
106
+ *
107
+ * @param folderWorkflows - The workflows that ship as skill folders; the default is the real
108
+ * set. They count as known skills, `shadow` refuses them, and `list` tags a leftover shadow
109
+ * of one as ignored. Tests pass a fixture set, because the real set is empty.
110
+ */
111
+ export async function runSkillsAction(action, name, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
93
112
  const devflowDir = getDevFlowDirectory();
94
113
  const claudeDir = getClaudeDirectory();
95
114
  // Union FEATURE_OWNED_SKILLS (compliance) so shadow/unshadow/list cover feature-managed
96
115
  // skills that left the plugin registry but still live in src/assets/skills/ (step 1.5).
97
- const allSkills = [...getAllSkillNames(), ...FEATURE_OWNED_SKILLS];
116
+ const allSkills = [...getAllSkillNames(folderWorkflows), ...FEATURE_OWNED_SKILLS];
98
117
  if (action === 'shadow') {
99
118
  if (!name) {
100
119
  p.log.error('Skill name required. Usage: devflow skills shadow <name>');
@@ -103,6 +122,11 @@ export const skillsCommand = new Command('skills')
103
122
  }
104
123
  // Accept both bare and prefixed input
105
124
  const bareName = unprefixSkillName(name);
125
+ const refusal = workflowFolderShadowRefusal(bareName, folderWorkflows);
126
+ if (refusal !== null) {
127
+ p.log.error(refusal);
128
+ process.exit(1);
129
+ }
106
130
  if (!allSkills.includes(bareName)) {
107
131
  p.log.error(`Unknown skill: ${bareName}`);
108
132
  p.log.info(`Available skills: ${allSkills.join(', ')}`);
@@ -183,7 +207,11 @@ export const skillsCommand = new Command('skills')
183
207
  const provenance = installed
184
208
  ? color.dim(`installed because: ${describeOwners(skill, ownersBySkill.get(skill))}`)
185
209
  : color.dim(`not installed — provided by: ${describeOwners(skill, ownersBySkill.get(skill))}`);
186
- return ` ${color.cyan(skill.padEnd(28))} ${buildSkillShadowTag(shadowState).padEnd(20)} ${provenance}`;
210
+ // A shadow of a skill folder is kept but never applied (D-WORKFLOW-SHADOW-REFUSAL).
211
+ const tag = workflowFolderShadowRefusal(skill, folderWorkflows) !== null && shadowState !== 'none'
212
+ ? color.yellow('shadow ignored — skill folder')
213
+ : buildSkillShadowTag(shadowState);
214
+ return ` ${color.cyan(skill.padEnd(28))} ${tag.padEnd(20)} ${provenance}`;
187
215
  });
188
216
  // Orphan shadows: in ~/.devflow/skills/ but not a known skill
189
217
  for (const dirName of shadowDirNames) {
@@ -200,5 +228,10 @@ export const skillsCommand = new Command('skills')
200
228
  p.log.info('Usage: devflow skills <shadow|unshadow|list> [name]');
201
229
  process.exit(1);
202
230
  }
203
- });
231
+ }
232
+ export const skillsCommand = new Command('skills')
233
+ .description('Manage skill overrides (shadow/unshadow/list)')
234
+ .argument('<action>', 'Action: shadow, unshadow, or list')
235
+ .argument('[name]', 'Skill name (required for shadow/unshadow)')
236
+ .action((action, name) => runSkillsAction(action, name));
204
237
  //# sourceMappingURL=skills.js.map
@@ -56,12 +56,18 @@ export async function readTrackerProvenance(devflowDir, provider) {
56
56
  * every install carries them all (D-INSTALL-ALL-PROVIDERS) — rather than listing
57
57
  * the directory: the question a user asks `--status` is "can the agent load what
58
58
  * it is told to load?", and a stray file in the tree is not an answer to it.
59
+ *
60
+ * `compliance` is the machine's compliance switch, read from the install manifest by the
61
+ * caller and never from a repository file: a compliance-off machine installs the compliance-off
62
+ * manifest, which holds fewer references, so counting against the compliance-on one would read
63
+ * every such machine as damaged.
64
+ *
59
65
  * Never throws.
60
66
  */
61
- export async function readTrackerMechanics(claudeDir) {
67
+ export async function readTrackerMechanics(claudeDir, compliance) {
62
68
  const root = path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references');
63
69
  let present = 0;
64
- for (const rel of installedReferenceManifest()) {
70
+ for (const rel of installedReferenceManifest({ compliance })) {
65
71
  try {
66
72
  await fs.access(path.join(root, ...rel.split('/')));
67
73
  present++;
@@ -292,7 +298,7 @@ export const trackerCommand = new Command('tracker')
292
298
  const selection = repoTrackerSelection(settingsModule, { dir: process.cwd() });
293
299
  const effective = selection?.provider ?? current.provider;
294
300
  const conventions = await readTrackerStatusConventions(devflowDir, effective);
295
- const mechanics = await readTrackerMechanics(getClaudeDirectory());
301
+ const mechanics = await readTrackerMechanics(getClaudeDirectory(), manifest.features.compliance.enabled);
296
302
  // [D-F] Inspecting the status re-arms the attempt counters. --status is
297
303
  // the command a capped user reaches for to find out why nothing is being
298
304
  // learned, so it is the command that has to hand back another five