devflow-kit 3.7.0 → 3.9.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 (205) hide show
  1. package/CHANGELOG.md +479 -1
  2. package/README.md +15 -15
  3. package/dist/agents/code.md +6 -8
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +7 -8
  6. package/dist/agents/git.md +3 -3
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +2 -4
  9. package/dist/agents/review.md +4 -4
  10. package/dist/agents/skim.md +2 -2
  11. package/dist/agents/triage.md +9 -2
  12. package/dist/cli/commands/capture.js +1 -1
  13. package/dist/cli/commands/curation-move-io.js +102 -0
  14. package/dist/cli/commands/curation-move-step.js +374 -0
  15. package/dist/cli/commands/curation.js +808 -0
  16. package/dist/cli/commands/gate-level-prompts.js +428 -0
  17. package/dist/cli/commands/gates.js +35 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +244 -57
  20. package/dist/cli/commands/memory.js +60 -13
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +157 -17
  23. package/dist/cli.js +2 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +40 -44
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +26 -25
  28. package/dist/commands/dynamic-plan.md +5 -5
  29. package/dist/commands/dynamic-profile.md +5 -5
  30. package/dist/commands/dynamic-tickets.md +5 -5
  31. package/dist/commands/explore.md +6 -6
  32. package/dist/commands/implement.md +53 -82
  33. package/dist/commands/plan.md +6 -6
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +39 -41
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/area-pause-notice.js +44 -0
  39. package/dist/core/assets.js +20 -20
  40. package/dist/core/curate-tuning-config.js +5 -6
  41. package/dist/core/curation-move-notice.js +84 -0
  42. package/dist/core/curation-move-skip.js +35 -0
  43. package/dist/core/curation-move.js +35 -0
  44. package/dist/core/curation-queue-cleanup.js +135 -0
  45. package/dist/core/curation-store.js +99 -0
  46. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  47. package/dist/core/echo-value.js +11 -5
  48. package/dist/core/evidence-policy.js +11 -9
  49. package/dist/core/feature-switch.js +95 -19
  50. package/dist/core/fs-atomic.js +1 -1
  51. package/dist/core/gate-level.js +10 -2
  52. package/dist/core/git.js +16 -14
  53. package/dist/core/ledger-root.js +82 -22
  54. package/dist/core/linked-path.js +4 -4
  55. package/dist/core/manifest-lock.js +238 -0
  56. package/dist/core/manifest.js +58 -14
  57. package/dist/core/migrations.js +1 -13
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +19 -19
  60. package/dist/core/pool.js +26 -0
  61. package/dist/core/project-paths.js +103 -34
  62. package/dist/core/queue-drain.js +2 -2
  63. package/dist/core/tracked-areas.js +159 -0
  64. package/dist/core/tracker.js +1 -1
  65. package/dist/{learning-off → curation-off}/agents/code.md +6 -8
  66. package/dist/{learning-off → curation-off}/agents/fix.md +7 -8
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  68. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +8 -1
  70. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  71. package/dist/{learning-off → curation-off}/commands/code-review.md +37 -41
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +23 -22
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +50 -79
  76. package/dist/{learning-off → curation-off}/commands/plan.md +2 -2
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +36 -38
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +63 -17
  80. package/dist/hud/config.js +1 -1
  81. package/dist/hud/index.js +6 -6
  82. package/dist/hud/render.js +3 -3
  83. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  84. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  85. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  86. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  87. package/dist/skills/git/references/publication-gate.md +6 -7
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  89. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  90. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  91. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  92. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  94. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  95. package/dist/targets/claude-code/installer.js +15 -15
  96. package/dist/targets/claude-code/language-stamp.js +5 -5
  97. package/dist/targets/claude-code/post-install.js +2 -2
  98. package/dist/targets/claude-code/tracker-servers.js +1 -1
  99. package/package.json +3 -2
  100. package/src/assets/agents/_partials/_code.mds +11 -11
  101. package/src/assets/agents/code.mds +6 -6
  102. package/src/assets/agents/curate.md +8 -8
  103. package/src/assets/agents/debug.mds +6 -6
  104. package/src/assets/agents/design.mds +18 -18
  105. package/src/assets/agents/evaluate.md +6 -12
  106. package/src/assets/agents/fix.mds +7 -6
  107. package/src/assets/agents/git.mds +3 -3
  108. package/src/assets/agents/harden.mds +7 -7
  109. package/src/assets/agents/knowledge.mds +23 -23
  110. package/src/assets/agents/refactor.mds +2 -2
  111. package/src/assets/agents/research.mds +15 -15
  112. package/src/assets/agents/review.mds +43 -43
  113. package/src/assets/agents/skim.mds +11 -11
  114. package/src/assets/agents/triage.mds +29 -22
  115. package/src/assets/agents/validate.md +30 -50
  116. package/src/assets/commands/_partials/_decisions.mds +22 -20
  117. package/src/assets/commands/_partials/_engine.mds +8 -8
  118. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  119. package/src/assets/commands/_partials/_preamble.mds +4 -4
  120. package/src/assets/commands/_partials/_settings.mds +3 -3
  121. package/src/assets/commands/_partials/_wave.mds +7 -7
  122. package/src/assets/commands/bug-analysis.mds +37 -48
  123. package/src/assets/commands/code-review.mds +53 -57
  124. package/src/assets/commands/debug.mds +14 -14
  125. package/src/assets/commands/dynamic-build.mds +38 -37
  126. package/src/assets/commands/dynamic-plan.mds +22 -22
  127. package/src/assets/commands/dynamic-profile.mds +2 -2
  128. package/src/assets/commands/dynamic-tickets.mds +8 -8
  129. package/src/assets/commands/explore.mds +20 -20
  130. package/src/assets/commands/implement.mds +77 -98
  131. package/src/assets/commands/plan.mds +27 -27
  132. package/src/assets/commands/release.mds +12 -12
  133. package/src/assets/commands/research.mds +19 -19
  134. package/src/assets/commands/resolve.mds +66 -60
  135. package/src/assets/commands/self-review.mds +20 -20
  136. package/src/assets/mds/git/_ops.mds +9 -2
  137. package/src/assets/mds/git/_ops_pr.mds +21 -11
  138. package/src/assets/mds/git/_ops_steps.mds +20 -4
  139. package/src/assets/mds/git/_pr.mds +21 -18
  140. package/src/assets/mds/git/_references.mds +6 -7
  141. package/src/assets/mds/tracker/_github.mds +1 -4
  142. package/src/assets/mds/tracker/_jira.mds +1 -4
  143. package/src/assets/mds/tracker/_linear.mds +1 -4
  144. package/src/assets/rules/quality.md +1 -0
  145. package/src/assets/scripts/ci-wait.cjs +389 -92
  146. package/src/assets/scripts/hooks/background-memory-update +81 -3
  147. package/src/assets/scripts/hooks/capture-prompt +12 -12
  148. package/src/assets/scripts/hooks/capture-question +11 -11
  149. package/src/assets/scripts/hooks/capture-turn +12 -12
  150. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  151. package/src/assets/scripts/hooks/curation-move +293 -0
  152. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  153. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  154. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  155. package/src/assets/scripts/hooks/git-marker +81 -0
  156. package/src/assets/scripts/hooks/json-helper.cjs +176 -51
  157. package/src/assets/scripts/hooks/json-parse +1 -1
  158. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  159. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +308 -122
  160. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  161. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  162. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  163. package/src/assets/scripts/hooks/lib/render-decisions.cjs +42 -22
  164. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +452 -0
  165. package/src/assets/scripts/hooks/memory-permission +10 -2
  166. package/src/assets/scripts/hooks/memory-worker +12 -1
  167. package/src/assets/scripts/hooks/pre-compact-memory +17 -16
  168. package/src/assets/scripts/hooks/queue-append +444 -102
  169. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  170. package/src/assets/scripts/hooks/session-start-context +198 -72
  171. package/src/assets/scripts/hooks/session-start-memory +42 -43
  172. package/src/assets/scripts/lib/project-config.cjs +12 -5
  173. package/src/assets/scripts/pr-evidence.cjs +5 -2
  174. package/src/assets/scripts/resolve-base.cjs +1043 -0
  175. package/src/assets/scripts/resolve-settings.cjs +283 -45
  176. package/src/assets/scripts/verify-evidence.cjs +307 -75
  177. package/src/assets/skills/agent-references/SKILL.md +11 -2
  178. package/src/assets/skills/agent-references/references/commands/implement-levels.md +29 -0
  179. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  180. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +25 -0
  181. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  182. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  183. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  184. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  185. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  186. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  187. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  188. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  189. package/templates/lint-pack/.lint-pack.conf +86 -0
  190. package/templates/lint-pack/README.md +190 -0
  191. package/templates/lint-pack/lint-pack.sh +905 -0
  192. package/dist/cli/commands/learning.js +0 -554
  193. package/dist/core/learning-queue-cleanup.js +0 -27
  194. package/dist/core/learning-store.js +0 -61
  195. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  196. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  197. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  198. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  199. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  200. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  201. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  202. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  203. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  204. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  205. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -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';
@@ -40,7 +43,7 @@ const MEMORY_HOOK_CONFIG = {
40
43
  *
41
44
  * D-EXACT-HOOK-OWNER (hooks.ts): matched as command ENDINGS under any directory —
42
45
  * the v1 (≤ v1.2) direct `.sh` scripts, then the retired `run-hook` markers of the
43
- * prompt-capture, learning, decisions, knowledge-refresh, sidecar and dream
46
+ * prompt-capture, curation, decisions, knowledge-refresh, sidecar and dream
44
47
  * pipelines — never as substrings, so a user's hook that mentions one is theirs.
45
48
  */
46
49
  const LEGACY_HOOK_SUFFIXES = {
@@ -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 drainLearningQueue.
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
  });
@@ -9,8 +9,45 @@
9
9
  * D-PROMPT-IO: WizardPromptIO is the base DI seam for all two-action wizard
10
10
  * steps (note + boolean select). Modules that add a third prompt extend this
11
11
  * interface with an intersection type (e.g. CompliancePromptIO).
12
+ *
13
+ * The shared wizard gate predicate (shouldRunPromptedWizardStep) lives here too,
14
+ * beside the seam every prompted step already imports.
12
15
  */
13
16
  import * as p from '@clack/prompts';
17
+ // ── Shared gate predicate ─────────────────────────────────────────────────────
18
+ /**
19
+ * Determines whether a wizard step that runs on both wizard paths should run for a
20
+ * given init invocation.
21
+ *
22
+ * Gate table (key on modePromptShown, never on the mode name):
23
+ *
24
+ * --recommended flag / !isTTY fallback → no (promptless contract preserved)
25
+ * Interactive mode-prompt → Recommended → yes (modePromptShown=true)
26
+ * --advanced flag / re-init (banner path) → yes (mode='advanced', isTTY=true)
27
+ * Interactive mode-prompt → Advanced → yes (modePromptShown=true)
28
+ * Any path with the step's CLI flag → no (hasCliOverride wins)
29
+ *
30
+ * A step that wires only the Recommended path is dead on every re-init, because re-init
31
+ * is Advanced-only by construction, so each step calls this predicate from both paths.
32
+ * The gate-level step delegates here. The attribution step is Advanced-only by design
33
+ * and does not use this table.
34
+ *
35
+ * Pure predicate — no side effects, fully testable without a TTY.
36
+ */
37
+ export function shouldRunPromptedWizardStep(input) {
38
+ if (input.hasCliOverride)
39
+ return false;
40
+ if (!input.isTTY)
41
+ return false;
42
+ // Advanced path: non-TTY has already exit-1'd, so isTTY=true here → always run.
43
+ // Covers: --advanced flag, re-init banner path, interactive-prompt → advanced.
44
+ if (input.mode === 'advanced')
45
+ return true;
46
+ // Recommended path: only run when the Setup-mode p.select actually ran
47
+ // (user made an active choice). --recommended flag and !isTTY fallback never set
48
+ // modePromptShown=true, preserving their promptless contracts.
49
+ return input.modePromptShown;
50
+ }
14
51
  // ── Shared clack adapters ─────────────────────────────────────────────────────
15
52
  /** Real clack adapter for the note prompt. */
16
53
  export function clackNote(message, title) {
@@ -31,6 +31,9 @@ import { stripFlags } from '../../core/flags.js';
31
31
  import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
32
32
  import { getPackageRoot, isContainedIn } from '../../core/paths.js';
33
33
  import { firstSymbolicLink } from '../../core/linked-path.js';
34
+ import { MANIFEST_LOCK_NAME, MANIFEST_LOCK_STALE_MARK } from '../../core/manifest-lock.js';
35
+ import { CURATION_DIR_NAME, CURATION_TUNING_FILE_NAME, LEGACY_NAMES, MOVE_JOURNAL_SUFFIX, MOVE_LOCK_PREFIX, MOVE_LOCK_SUFFIX, getMemoryDir } from '../../core/project-paths.js';
36
+ import { probeTrackedArea } from '../../core/tracked-areas.js';
34
37
  import { restampInstalledCommands } from '../../targets/claude-code/language-stamp.js';
35
38
  import { installCommandsAndAgents } from '../../targets/claude-code/installer.js';
36
39
  /**
@@ -283,20 +286,66 @@ const TRACKED_ENTRIES = DEVFLOW_TRACKED_PATHS.map((tracked) => ({
283
286
  name: tracked.replace(/\/$/, ''),
284
287
  isDir: tracked.endsWith('/'),
285
288
  }));
289
+ /** The memory folder's name under `.devflow/`, by the name the path helper gives it. */
290
+ const MEMORY_FOLDER_NAME = path.basename(getMemoryDir(path.sep));
291
+ /**
292
+ * The data area an entry under `.devflow/` belongs to, or null. The curation area holds the
293
+ * curation folder, the old ledger folder and the backup folders; the memory area holds the
294
+ * memory folder. Decided by name alone, whatever the kind of entry, and without regard to
295
+ * ASCII letter case, as the tracked-areas check matches them.
296
+ */
297
+ function projectDataAreaOf(name) {
298
+ const lower = name.toLowerCase();
299
+ if (lower === CURATION_DIR_NAME || lower === LEGACY_NAMES.folder || lower.startsWith(LEGACY_NAMES.backupPrefix))
300
+ return 'curation';
301
+ if (lower === MEMORY_FOLDER_NAME)
302
+ return 'memory';
303
+ return null;
304
+ }
286
305
  /**
287
306
  * Split the entries under a repository's `.devflow/` into what a confirmed cleanup
288
307
  * removes and what it keeps (D-UNINSTALL-CARVE-OUT). An entry is kept only when both
289
308
  * its name and its kind match a tracked path, so a stray FILE named `features` is
290
- * not mistaken for the tracked directory. PURE — both lists sorted by name.
309
+ * not mistaken for the tracked directory. An entry of a paused data area (`pausedAreas`:
310
+ * git tracks files in the area's folders, or did not answer) is kept too, in
311
+ * `keepPaused`. PURE — all three lists sorted by name.
291
312
  */
292
- export function partitionProjectData(entries) {
313
+ export function partitionProjectData(entries, pausedAreas = new Set()) {
293
314
  const isTracked = (entry) => TRACKED_ENTRIES.some((t) => t.name === entry.name && t.isDir === entry.isDir);
315
+ const isPaused = (entry) => {
316
+ const area = projectDataAreaOf(entry.name);
317
+ return area !== null && pausedAreas.has(area);
318
+ };
294
319
  const byName = (a, b) => a.name.localeCompare(b.name);
295
320
  return {
296
- remove: entries.filter((e) => !isTracked(e)).sort(byName),
321
+ remove: entries.filter((e) => !isTracked(e) && !isPaused(e)).sort(byName),
297
322
  keep: entries.filter(isTracked).sort(byName),
323
+ keepPaused: entries.filter((e) => !isTracked(e) && isPaused(e)).sort(byName),
298
324
  };
299
325
  }
326
+ /** The check's own answer, from the package's own copy of it. */
327
+ const probeProjectDataArea = async (root, area) => (await probeTrackedArea(root, area)).answer;
328
+ /**
329
+ * The data areas that hold an entry in `entries` and are paused at `gitRoot`: git tracks
330
+ * files in the area's folders, or a `.git` entry stands and git did not answer. An area
331
+ * with no entry is not asked, and a root with no `.git` entry makes no git call. A probe
332
+ * that fails to answer counts as `unknown`, so the area is kept.
333
+ */
334
+ async function pausedProjectDataAreas(gitRoot, entries, probe) {
335
+ const present = new Set(entries.flatMap((entry) => {
336
+ const area = projectDataAreaOf(entry.name);
337
+ return area === null ? [] : [area];
338
+ }));
339
+ const answers = await Promise.all([...present].map(async (area) => {
340
+ try {
341
+ return [area, await probe(gitRoot, area)];
342
+ }
343
+ catch {
344
+ return [area, 'unknown'];
345
+ }
346
+ }));
347
+ return new Set(answers.flatMap(([area, answer]) => (answer === 'untracked' ? [] : [area])));
348
+ }
300
349
  /**
301
350
  * Resolve the project-data step's target: `<gitRoot>/.devflow`, partitioned.
302
351
  *
@@ -310,6 +359,12 @@ export function partitionProjectData(entries) {
310
359
  * A `.devflow` that is a symbolic link to anywhere else is skipped too, and never
311
360
  * followed: its target is a directory devflow cannot prove it wrote, so a
312
361
  * confirmed cleanup must not empty it — nor unlink a link the user made.
362
+ *
363
+ * A folder that git tracks is the repository's content, not devflow's data. The
364
+ * curation area (the curation folder, the old ledger folder and the backup folders)
365
+ * and the memory area (the memory folder) are each asked of the tracked-areas check at
366
+ * the git root, when an entry of the area stands. An area that git tracks files in, or
367
+ * that git did not answer for, moves its entries from the removal list to `keepPaused`.
313
368
  */
314
369
  export async function resolveProjectDataPlan(opts) {
315
370
  if (opts.gitRoot === null)
@@ -333,8 +388,10 @@ export async function resolveProjectDataPlan(opts) {
333
388
  catch {
334
389
  return { kind: 'skip', reason: 'absent' };
335
390
  }
336
- const { remove, keep } = partitionProjectData(dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() })));
337
- return { kind: 'plan', plan: { dir, remove, keep } };
391
+ const entries = dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() }));
392
+ const paused = await pausedProjectDataAreas(opts.gitRoot, entries, opts.probe ?? probeProjectDataArea);
393
+ const { remove, keep, keepPaused } = partitionProjectData(entries, paused);
394
+ return { kind: 'plan', plan: { dir, remove, keep, keepPaused } };
338
395
  }
339
396
  /**
340
397
  * The line that tells the user why the project-data step was skipped, or null
@@ -364,15 +421,21 @@ function entryLabel(entry) {
364
421
  function entryList(entries) {
365
422
  return entries.length === 0 ? '(none)' : entries.map(entryLabel).join(', ');
366
423
  }
424
+ /** Why the entries of a data area stay, said beside a list of them. */
425
+ const KEEP_PAUSED_REASON = 'git tracks them, or git did not answer';
426
+ /** Why one entry of a data area stays, said beside it. */
427
+ const KEEP_PAUSED_REASON_ONE = 'git tracks it, or git did not answer';
367
428
  /**
368
429
  * The lines shown before the project-data confirm: what is removed and what is
369
- * kept. PURE.
430
+ * kept, and, only where there are such entries, what is kept because git tracks them
431
+ * or did not answer. PURE.
370
432
  */
371
433
  export function formatProjectDataPlan(plan) {
372
434
  return [
373
435
  `Project data in ${plan.dir}/:`,
374
436
  ` Remove: ${entryList(plan.remove)}`,
375
437
  ` Keep (shared via git): ${entryList(plan.keep)}`,
438
+ ...(plan.keepPaused.length === 0 ? [] : [` Keep (${KEEP_PAUSED_REASON}): ${entryList(plan.keepPaused)}`]),
376
439
  ];
377
440
  }
378
441
  /**
@@ -489,8 +552,10 @@ export function userContentPaths(devflowDir) {
489
552
  const name = `${TRACKER_LEGACY_CONVENTIONS_FILE}.${id}.bak`;
490
553
  return { relPath: name, label: `${name} (previous issue tracker conventions)` };
491
554
  }),
492
- // learning.json — global curate agent tuning config
493
- { relPath: 'learning.json', label: 'learning.json' },
555
+ // The global curate agent tuning config, under both its names: the new name on an
556
+ // upgraded machine, and the old name, which a downgraded machine can still hold.
557
+ { relPath: CURATION_TUNING_FILE_NAME, label: CURATION_TUNING_FILE_NAME },
558
+ { relPath: LEGACY_NAMES.tuningFile, label: LEGACY_NAMES.tuningFile },
494
559
  // hud.json — user HUD enable/disable preference and display config
495
560
  { relPath: 'hud.json', label: 'hud.json (HUD configuration)' },
496
561
  ];
@@ -527,6 +592,54 @@ export async function enumerateUserDevFlowContent(devflowDir) {
527
592
  }
528
593
  return items;
529
594
  }
595
+ /** `text` as a regular-expression source, every character literal. */
596
+ function escapeRegExp(text) {
597
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
598
+ }
599
+ /** The name of a move lock or journal: the shared prefix, a 16-digit lowercase hex key, then `suffix`. */
600
+ function moveFileShape(suffix) {
601
+ return new RegExp(`^${escapeRegExp(MOVE_LOCK_PREFIX)}[0-9a-f]{16}${escapeRegExp(suffix)}$`);
602
+ }
603
+ /**
604
+ * The curation move's lock, one directory per ledger root, taken directly in the machine
605
+ * directory: `.curation-move-<16 lowercase hex>.lock`. The store builds the name from a
606
+ * hash of the real ledger root, so the names exist only on disk. The prefix and the
607
+ * suffix are the ones the path helpers hold (project-paths.ts).
608
+ */
609
+ const CURATION_MOVE_LOCK_SHAPE = moveFileShape(MOVE_LOCK_SUFFIX);
610
+ /**
611
+ * The curation move's journal, one regular file per ledger root, beside the lock:
612
+ * `.curation-move-<16 lowercase hex>.journal`, with the same key as the lock. It exists
613
+ * only while a set move is unfinished, and a crash leaves it behind. A file named
614
+ * `.curation-move.journal` inside a project's old ledger folder is no journal of the
615
+ * move and is not matched here.
616
+ */
617
+ const CURATION_MOVE_JOURNAL_SHAPE = moveFileShape(MOVE_JOURNAL_SUFFIX);
618
+ /**
619
+ * The manifest lock, a directory in the machine directory (manifest-lock.ts), and the
620
+ * directory a stale break moves aside: `manifest.json.lock` and
621
+ * `manifest.json.lock.stale-<digits>-<12 lowercase hex>`. A holder that dies in its
622
+ * critical section leaves the first, a break whose removal failed leaves the second.
623
+ */
624
+ const MANIFEST_LOCK_SHAPE = new RegExp(`^${escapeRegExp(MANIFEST_LOCK_NAME)}(?:${escapeRegExp(MANIFEST_LOCK_STALE_MARK)}[0-9]+-[0-9a-f]{12})?$`);
625
+ /** Whether `child` of `devflowDir` is a real directory (a link is not followed). */
626
+ async function isRealDirectory(devflowDir, child) {
627
+ try {
628
+ return (await fs.lstat(path.join(devflowDir, child))).isDirectory();
629
+ }
630
+ catch {
631
+ return false;
632
+ }
633
+ }
634
+ /** Whether `child` of `devflowDir` is a regular file (a link is not followed). */
635
+ async function isRegularFile(devflowDir, child) {
636
+ try {
637
+ return (await fs.lstat(path.join(devflowDir, child))).isFile();
638
+ }
639
+ catch {
640
+ return false;
641
+ }
642
+ }
530
643
  /**
531
644
  * Expand `installArtifactPaths` against real disk.
532
645
  *
@@ -553,8 +666,15 @@ export async function resolveInstallArtifactPaths(devflowDir) {
553
666
  }
554
667
  }
555
668
  for (const child of children) {
556
- if (child.startsWith(entry.relPath))
557
- resolved.push({ relPath: child, isDir: entry.isDir });
669
+ if (!child.startsWith(entry.relPath))
670
+ continue;
671
+ if (entry.shape !== undefined && !entry.shape.test(child))
672
+ continue;
673
+ if (entry.realDirOnly === true && !(await isRealDirectory(devflowDir, child)))
674
+ continue;
675
+ if (entry.realFileOnly === true && !(await isRegularFile(devflowDir, child)))
676
+ continue;
677
+ resolved.push({ relPath: child, isDir: entry.isDir });
558
678
  }
559
679
  }
560
680
  return resolved;
@@ -621,6 +741,18 @@ export function installArtifactPaths(devflowDir) {
621
741
  // A prefix, because the names exist only on disk. Content is a scrubbed copy
622
742
  // that was never placed, so it is machine state like the three above.
623
743
  { relPath: TRACKER_STAGED_PREFIX, isPrefix: true },
744
+ // The curation move's per-root lock directory. A crash between taking the lock and
745
+ // releasing it leaves one behind, and its name carries a hash that exists only on disk.
746
+ // Only a real directory of exactly that shape goes; the move's skip markers sit under
747
+ // logs/ and go with it.
748
+ { relPath: MOVE_LOCK_PREFIX, isPrefix: true, isDir: true, shape: CURATION_MOVE_LOCK_SHAPE, realDirOnly: true },
749
+ // The move's journal beside it. A crash between two renames of the ledger set leaves
750
+ // one behind. Only a regular file of exactly that shape goes.
751
+ { relPath: MOVE_LOCK_PREFIX, isPrefix: true, shape: CURATION_MOVE_JOURNAL_SHAPE, realFileOnly: true },
752
+ // The manifest lock, and the directory a stale break moved aside. A crash inside the
753
+ // critical section leaves the first; a failed cleanup leaves the second, whose name
754
+ // carries a pid and a random suffix. Only a real directory of exactly that shape goes.
755
+ { relPath: MANIFEST_LOCK_NAME, isPrefix: true, isDir: true, shape: MANIFEST_LOCK_SHAPE, realDirOnly: true },
624
756
  // per-project hook logs (logs/{project-slug}/) AND global logs — remove the
625
757
  // whole logs/ tree; covers proxy.log, debug logs, and any project-slug dirs.
626
758
  { relPath: 'logs', isDir: true },
@@ -644,7 +776,8 @@ export function installArtifactPaths(devflowDir) {
644
776
  * so an entry here is deleted even when the user answers "no" to the full wipe.
645
777
  * User-authored state (everything in `userContentPaths`: skill/rule shadows,
646
778
  * preference-profile.md, the tracker conventions with the legacy tracker.md and
647
- * its backups, learning.json, hud.json) is removed only by the confirmed full-dir rm.
779
+ * its backups, the global tuning file under both names, hud.json) is removed only by
780
+ * the confirmed full-dir rm.
648
781
  * agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
649
782
  * re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
650
783
  * belongs in this list, not in enumerateUserDevFlowContent.
@@ -874,6 +1007,9 @@ export async function runDryRunPhase(opts) {
874
1007
  for (const entry of projectData.plan.remove) {
875
1008
  extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (if confirmed)`);
876
1009
  }
1010
+ for (const entry of projectData.plan.keepPaused) {
1011
+ extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (kept: ${KEEP_PAUSED_REASON_ONE})`);
1012
+ }
877
1013
  }
878
1014
  else {
879
1015
  const skipped = formatProjectDataSkip(projectData.reason, projectRoot);
@@ -953,7 +1089,7 @@ export async function refreshRetainedCommandsAndAgents(opts) {
953
1089
  plugins: retained,
954
1090
  agentsMap: buildAssetMaps(retained).agentsMap,
955
1091
  claudeDir,
956
- learning: manifest.features.learning,
1092
+ curation: manifest.features.curation,
957
1093
  trackerServers: effectiveTrackerServers(manifest.features.tracker),
958
1094
  warn: (msg) => p.log.warn(msg),
959
1095
  });
@@ -1073,7 +1209,7 @@ export async function runSelectivePhaseForScope(opts) {
1073
1209
  * FULL PHASE (per scope): revert agent model overrides, remove all Devflow
1074
1210
  * assets from the scope, then perform scope-appropriate devflowDir cleanup.
1075
1211
  *
1076
- * Local scope: remove only install artifacts — project data (memory, learning,
1212
+ * Local scope: remove only install artifacts — project data (memory, curation,
1077
1213
  * docs, features) is always preserved under the git root.
1078
1214
  * User scope: interactive TTY with user-authored content → confirm before wiping
1079
1215
  * ~/.devflow/; non-interactive or no user content → artifacts-only.
@@ -1115,11 +1251,11 @@ export async function runFullPhaseForScope(opts) {
1115
1251
  await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange);
1116
1252
  if (scope === 'local') {
1117
1253
  // Local scope: devflowDir is gitRoot/.devflow/ which holds project data
1118
- // (memory, learning, features, docs, config.json). Never remove those —
1254
+ // (memory, curation, features, docs, config.json). Never remove those —
1119
1255
  // only remove install artifacts: scripts/ (done above) + manifest.json and
1120
1256
  // the other Devflow-generated artifacts (see removeDevFlowInstallArtifacts).
1121
1257
  await removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange);
1122
- p.log.info('Local project data (memory, learning, features, docs) preserved');
1258
+ p.log.info('Local project data (memory, curation, features, docs) preserved');
1123
1259
  }
1124
1260
  else {
1125
1261
  // User scope (devflowDir = ~/.devflow/): offer full cleanup behind a confirm gate
@@ -1179,7 +1315,9 @@ export async function runFullPhaseForScope(opts) {
1179
1315
  async function runProjectDataStep(plan, gates) {
1180
1316
  const shown = `${plan.dir}/`;
1181
1317
  if (plan.remove.length === 0) {
1182
- p.log.info(`${shown} preserved (holds only files shared via git)`);
1318
+ p.log.info(plan.keepPaused.length === 0
1319
+ ? `${shown} preserved (holds only files shared via git)`
1320
+ : `${shown} preserved (holds only files shared via git, and ${entryList(plan.keepPaused)}, which stay: ${KEEP_PAUSED_REASON})`);
1183
1321
  return;
1184
1322
  }
1185
1323
  if (gates.keepDocs) {
@@ -1215,7 +1353,7 @@ async function runProjectDataStep(plan, gates) {
1215
1353
  failed.push(`${entryLabel(entry)} (${err.code ?? err.message})`);
1216
1354
  }
1217
1355
  }
1218
- if (plan.keep.length === 0 && failed.length === 0) {
1356
+ if (plan.keep.length === 0 && plan.keepPaused.length === 0 && failed.length === 0) {
1219
1357
  // Nothing tracked was there: drop the now-empty directory. rmdir refuses a
1220
1358
  // directory something wrote into meanwhile, which is then left in place.
1221
1359
  await fs.rmdir(plan.dir).catch(() => undefined);
@@ -1226,6 +1364,8 @@ async function runProjectDataStep(plan, gates) {
1226
1364
  p.log.warn(`Could not remove from ${shown}: ${failed.join(', ')}`);
1227
1365
  if (plan.keep.length > 0)
1228
1366
  p.log.info(`Kept: ${entryList(plan.keep)}`);
1367
+ if (plan.keepPaused.length > 0)
1368
+ p.log.info(`Kept: ${entryList(plan.keepPaused)} (${KEEP_PAUSED_REASON})`);
1229
1369
  }
1230
1370
  /**
1231
1371
  * CLEANUP PHASE: post-loop extras run only on full uninstall.
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import { skillsCommand } from './cli/commands/skills.js';
11
11
  import { hudCommand } from './cli/commands/hud.js';
12
12
  import { flagsCommand } from './cli/commands/flags.js';
13
13
  import { knowledgeCommand } from './cli/commands/knowledge/index.js';
14
- import { learningCommand } from './cli/commands/learning.js';
14
+ import { curationCommand } from './cli/commands/curation.js';
15
15
  import { rulesCommand } from './cli/commands/rules.js';
16
16
  import { debugCommand } from './cli/commands/debug.js';
17
17
  import { securityCommand } from './cli/commands/security.js';
@@ -42,7 +42,7 @@ program.addCommand(skillsCommand);
42
42
  program.addCommand(hudCommand);
43
43
  program.addCommand(flagsCommand);
44
44
  program.addCommand(knowledgeCommand);
45
- program.addCommand(learningCommand);
45
+ program.addCommand(curationCommand);
46
46
  program.addCommand(rulesCommand);
47
47
  program.addCommand(debugCommand);
48
48
  program.addCommand(securityCommand);