devflow-kit 3.7.0 → 3.8.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 (184) hide show
  1. package/CHANGELOG.md +359 -1
  2. package/README.md +11 -11
  3. package/dist/agents/code.md +2 -4
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +6 -7
  6. package/dist/agents/git.md +1 -1
  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 +2 -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/{learning.js → curation.js} +302 -99
  16. package/dist/cli/commands/gate-level-prompts.js +426 -0
  17. package/dist/cli/commands/gates.js +34 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +240 -54
  20. package/dist/cli/commands/memory.js +2 -2
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +81 -9
  23. package/dist/cli.js +2 -2
  24. package/dist/commands/bug-analysis.md +24 -35
  25. package/dist/commands/code-review.md +32 -36
  26. package/dist/commands/debug.md +4 -4
  27. package/dist/commands/dynamic-build.md +25 -24
  28. package/dist/commands/dynamic-plan.md +4 -4
  29. package/dist/commands/dynamic-profile.md +4 -4
  30. package/dist/commands/dynamic-tickets.md +4 -4
  31. package/dist/commands/explore.md +5 -5
  32. package/dist/commands/implement.md +52 -81
  33. package/dist/commands/plan.md +5 -5
  34. package/dist/commands/release.md +4 -4
  35. package/dist/commands/research.md +5 -5
  36. package/dist/commands/resolve.md +28 -30
  37. package/dist/commands/self-review.md +4 -12
  38. package/dist/core/assets.js +20 -20
  39. package/dist/core/curate-tuning-config.js +5 -6
  40. package/dist/core/curation-move-notice.js +84 -0
  41. package/dist/core/curation-move-skip.js +36 -0
  42. package/dist/core/curation-move.js +35 -0
  43. package/dist/core/curation-queue-cleanup.js +134 -0
  44. package/dist/core/curation-store.js +98 -0
  45. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  46. package/dist/core/echo-value.js +11 -5
  47. package/dist/core/evidence-policy.js +1 -1
  48. package/dist/core/feature-switch.js +95 -19
  49. package/dist/core/fs-atomic.js +1 -1
  50. package/dist/core/gate-level.js +10 -2
  51. package/dist/core/git.js +16 -14
  52. package/dist/core/ledger-root.js +82 -22
  53. package/dist/core/linked-path.js +4 -4
  54. package/dist/core/manifest-lock.js +238 -0
  55. package/dist/core/manifest.js +58 -14
  56. package/dist/core/migrations.js +1 -13
  57. package/dist/core/observations.js +2 -2
  58. package/dist/core/plugins.js +15 -15
  59. package/dist/core/pool.js +26 -0
  60. package/dist/core/project-paths.js +103 -34
  61. package/dist/core/queue-drain.js +2 -2
  62. package/dist/core/tracked-areas.js +35 -0
  63. package/dist/core/tracker.js +1 -1
  64. package/dist/{learning-off → curation-off}/agents/code.md +2 -4
  65. package/dist/{learning-off → curation-off}/agents/fix.md +6 -7
  66. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  67. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  68. package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
  69. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  70. package/dist/{learning-off → curation-off}/commands/code-review.md +29 -33
  71. package/dist/{learning-off → curation-off}/commands/debug.md +1 -1
  72. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +22 -21
  73. package/dist/{learning-off → curation-off}/commands/explore.md +1 -1
  74. package/dist/{learning-off → curation-off}/commands/implement.md +49 -78
  75. package/dist/{learning-off → curation-off}/commands/plan.md +1 -1
  76. package/dist/{learning-off → curation-off}/commands/resolve.md +25 -27
  77. package/dist/{learning-off → curation-off}/commands/self-review.md +1 -9
  78. package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
  79. package/dist/hud/config.js +1 -1
  80. package/dist/hud/index.js +6 -6
  81. package/dist/hud/render.js +3 -3
  82. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  83. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  84. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  85. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  86. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  87. package/dist/targets/claude-code/installer.js +15 -15
  88. package/dist/targets/claude-code/language-stamp.js +5 -5
  89. package/dist/targets/claude-code/post-install.js +2 -2
  90. package/dist/targets/claude-code/tracker-servers.js +1 -1
  91. package/package.json +3 -2
  92. package/src/assets/agents/_partials/_code.mds +11 -11
  93. package/src/assets/agents/code.mds +2 -2
  94. package/src/assets/agents/curate.md +8 -8
  95. package/src/assets/agents/debug.mds +6 -6
  96. package/src/assets/agents/design.mds +18 -18
  97. package/src/assets/agents/fix.mds +6 -5
  98. package/src/assets/agents/git.mds +1 -1
  99. package/src/assets/agents/harden.mds +7 -7
  100. package/src/assets/agents/knowledge.mds +23 -23
  101. package/src/assets/agents/refactor.mds +2 -2
  102. package/src/assets/agents/research.mds +15 -15
  103. package/src/assets/agents/review.mds +43 -43
  104. package/src/assets/agents/skim.mds +11 -11
  105. package/src/assets/agents/triage.mds +22 -22
  106. package/src/assets/agents/validate.md +13 -26
  107. package/src/assets/commands/_partials/_decisions.mds +13 -13
  108. package/src/assets/commands/_partials/_engine.mds +8 -8
  109. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  110. package/src/assets/commands/_partials/_preamble.mds +4 -4
  111. package/src/assets/commands/_partials/_settings.mds +2 -2
  112. package/src/assets/commands/_partials/_wave.mds +7 -7
  113. package/src/assets/commands/bug-analysis.mds +37 -48
  114. package/src/assets/commands/code-review.mds +46 -50
  115. package/src/assets/commands/debug.mds +14 -14
  116. package/src/assets/commands/dynamic-build.mds +38 -37
  117. package/src/assets/commands/dynamic-plan.mds +22 -22
  118. package/src/assets/commands/dynamic-profile.mds +2 -2
  119. package/src/assets/commands/dynamic-tickets.mds +8 -8
  120. package/src/assets/commands/explore.mds +20 -20
  121. package/src/assets/commands/implement.mds +77 -98
  122. package/src/assets/commands/plan.mds +27 -27
  123. package/src/assets/commands/release.mds +12 -12
  124. package/src/assets/commands/research.mds +19 -19
  125. package/src/assets/commands/resolve.mds +56 -50
  126. package/src/assets/commands/self-review.mds +20 -20
  127. package/src/assets/mds/git/_ops.mds +3 -1
  128. package/src/assets/mds/git/_ops_pr.mds +1 -1
  129. package/src/assets/mds/git/_ops_steps.mds +10 -4
  130. package/src/assets/mds/git/_pr.mds +7 -4
  131. package/src/assets/rules/quality.md +1 -0
  132. package/src/assets/scripts/hooks/background-memory-update +2 -2
  133. package/src/assets/scripts/hooks/capture-prompt +12 -12
  134. package/src/assets/scripts/hooks/capture-question +11 -11
  135. package/src/assets/scripts/hooks/capture-turn +12 -12
  136. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  137. package/src/assets/scripts/hooks/curation-move +293 -0
  138. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  139. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  140. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  141. package/src/assets/scripts/hooks/git-marker +81 -0
  142. package/src/assets/scripts/hooks/json-helper.cjs +134 -51
  143. package/src/assets/scripts/hooks/json-parse +1 -1
  144. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  145. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
  146. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  147. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  149. package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
  150. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
  151. package/src/assets/scripts/hooks/memory-worker +1 -1
  152. package/src/assets/scripts/hooks/pre-compact-memory +6 -16
  153. package/src/assets/scripts/hooks/queue-append +228 -103
  154. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  155. package/src/assets/scripts/hooks/session-start-context +143 -69
  156. package/src/assets/scripts/hooks/session-start-memory +3 -43
  157. package/src/assets/scripts/lib/project-config.cjs +12 -5
  158. package/src/assets/scripts/resolve-base.cjs +1043 -0
  159. package/src/assets/scripts/resolve-settings.cjs +42 -18
  160. package/src/assets/skills/agent-references/SKILL.md +5 -2
  161. package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
  162. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  163. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
  164. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  165. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  166. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  167. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  168. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  169. package/templates/lint-pack/.lint-pack.conf +86 -0
  170. package/templates/lint-pack/README.md +190 -0
  171. package/templates/lint-pack/lint-pack.sh +905 -0
  172. package/dist/core/learning-queue-cleanup.js +0 -27
  173. package/dist/core/learning-store.js +0 -61
  174. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  175. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  176. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  177. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  178. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  179. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  180. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  181. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  182. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  183. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  184. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -8,7 +8,11 @@ import { resolveInstallationPaths } from '../../targets/claude-code/claude-paths
8
8
  import { getGitRoot } from '../../core/git.js';
9
9
  import { isSameLocation, withoutHomeRoots } from '../../core/same-location.js';
10
10
  import { pruneHookLogDirs, MAX_HOOK_LOG_DIRS } from '../../core/hook-log-dirs.js';
11
- import { getLedgerRoot } from '../../core/ledger-root.js';
11
+ import { LEDGER_ROOT_TIMEOUT_MS, lookupLedgerRoot } from '../../core/ledger-root.js';
12
+ import { formatCurationStoreUnavailable } from '../../core/curation-store.js';
13
+ import { loadCurationMove } from '../../core/curation-move.js';
14
+ import { describeMoveText, describeOutcome, runInitCurationMove, shownSkipAt } from './curation-move-step.js';
15
+ import { realGlobalTuningMoveIO, realProjectLedgerMoveIO } from './curation-move-io.js';
12
16
  import { describeEchoedValue } from '../../core/echo-value.js';
13
17
  import { installViaFileCopy, composeScripts } from '../../targets/claude-code/installer.js';
14
18
  import { formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
@@ -34,7 +38,7 @@ import { convergeFlagsIntoSettings, countActiveFlags, readViewMode } from '../..
34
38
  import { addContextHook, removeContextHook, hasContextHook } from './context.js';
35
39
  import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
36
40
  import { writeManagedConfig, readConfigIfPresent, DEFAULT_CONFIG } from '../../core/feature-config.js';
37
- import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
41
+ import { drainDisabledCurationQueue, formatCurationNotDrained } from '../../core/curation-queue-cleanup.js';
38
42
  import { formatRefusedDrain } from '../../core/queue-drain.js';
39
43
  import { removeManagedDenyList, describeManagedDenyRemoval } from './security.js';
40
44
  import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs, resolvePluginsToInstall } from './init-seed.js';
@@ -44,9 +48,10 @@ import { shouldRunBehaviorStep, runBehaviorStep, buildClackBehaviorPrompts } fro
44
48
  import { convergeBehaviorFromManifest } from '../../targets/claude-code/behavior-install.js';
45
49
  import { applyTrackerSentinel, effectiveTrackerServers, parseTrackerId, rearmTrackerInference, DEFAULT_TRACKER_PROVIDER, } from '../../core/tracker.js';
46
50
  import { formatTrackerSummary, shouldRunTrackerStep, runTrackerStep, buildClackTrackerPrompts, } from './tracker-prompts.js';
51
+ import { buildClackGateLevelPrompts, carriedGateLevel, formatGateLevelRow, formatGateLevelSummary, readStoredGateLevel, resolveGateLevelForInit, resolveGateLevelInitState, } from './gate-level-prompts.js';
47
52
  import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
48
53
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
49
- import { convergeLearningVariants, installedVersionMatches, readRunningVersion } from '../../targets/claude-code/learning-install.js';
54
+ import { convergeCurationVariants, installedVersionMatches, readRunningVersion } from '../../targets/claude-code/curation-install.js';
50
55
  import { auditAndRecordClaudeMd, formatClaudeMdAuditNote, formatClaudeMdAuditUnavailable, } from '../../core/claude-md-audit.js';
51
56
  import * as os from 'os';
52
57
  // Re-export pure functions for tests (canonical source is post-install.ts)
@@ -68,7 +73,7 @@ import { getPackageRoot } from '../../core/paths.js';
68
73
  * migrations are vacuously applied per D37 semantics).
69
74
  *
70
75
  * Migrations are a one-time cleanup pass over ~/.devflow runtime data
71
- * (memory, learning, knowledge). They never touch the installer's
76
+ * (memory, curation, knowledge). They never touch the installer's
72
77
  * copy targets (skills, agents, rules, commands, scripts), so ordering
73
78
  * relative to installViaFileCopy carries no data dependency.
74
79
  *
@@ -428,9 +433,73 @@ export async function persistManifestThenConvergeTracker(opts) {
428
433
  };
429
434
  }
430
435
  /**
431
- * Drain this repo's memory and learning queues for each feature init switched
436
+ * The curation move step (#529): rename the global tuning file, then move each target
437
+ * project's ledger folder from the old name to the new one. The targets are the
438
+ * discovered projects (else the current repository) plus the current repository's own
439
+ * ledger root, which the step lists once. State-checked on every run, with no run-once
440
+ * record. Never throws: a project that cannot move is a line, not an exit code.
441
+ *
442
+ * Init runs it after the legacy sweep and BEFORE the manifest is persisted and the
443
+ * disabled-feature queues drain, so the drain sees the moved folder.
444
+ *
445
+ * The own ledger root is looked up within the shared budget (`lookupOwnLedgerRoot`). A
446
+ * lookup that times out is no ledger root: the step moves the other targets, and one
447
+ * skipped line names the repository, unless the repository is itself a project root,
448
+ * where the step's own lookup reports it.
449
+ */
450
+ export async function moveCurationData(opts, lookup = lookupLedgerRoot) {
451
+ const loaded = loadCurationMove();
452
+ const roots = projectRoots([...opts.discoveredProjects], opts.gitRoot);
453
+ const own = await lookupOwnLedgerRoot(opts.gitRoot, lookup);
454
+ const report = await runInitCurationMove({
455
+ projectRoots: roots,
456
+ ownLedgerRoot: own.root,
457
+ homeDir: opts.homeDir,
458
+ machineDir: opts.devflowDir,
459
+ }, {
460
+ ledger: loaded.ok ? realProjectLedgerMoveIO(loaded.value) : { unavailable: formatCurationStoreUnavailable(loaded.error) },
461
+ tuning: realGlobalTuningMoveIO(),
462
+ });
463
+ if (!own.timedOut || opts.gitRoot === null || roots.includes(opts.gitRoot))
464
+ return report;
465
+ const skipped = describeOutcome({ kind: 'skipped', root: opts.gitRoot, reason: 'git-root-timed-out' });
466
+ return { ...report, lines: [...report.lines, ...skipped] };
467
+ }
468
+ /**
469
+ * The ledger root of the current repository for init's own move and drain
470
+ * (D-LEDGER-MAIN-WORKTREE): one lookup from the working directory, bounded by
471
+ * LEDGER_ROOT_TIMEOUT_MS like the HUD's and the step's lookups, so a repository on a
472
+ * hung mount costs a bounded wait and never stalls init. A lookup that timed out is no
473
+ * root (`root` null, `timedOut` true), so the caller acts on nothing and says so.
474
+ * Outside a git repository (`gitRoot` null) nothing is asked.
475
+ */
476
+ export async function lookupOwnLedgerRoot(gitRoot, lookup = lookupLedgerRoot) {
477
+ if (gitRoot === null)
478
+ return { root: null, timedOut: false };
479
+ const found = await lookup(undefined, { timeoutMs: LEDGER_ROOT_TIMEOUT_MS });
480
+ return { root: found.kind === 'root' ? found.root : null, timedOut: found.kind === 'timed-out' };
481
+ }
482
+ /**
483
+ * The ledger root init's disabled-feature drain acts on, and the one line to print when
484
+ * there is none because git did not answer in time. Asks only when the drain can run: curation
485
+ * is off, in a git repository, and the manifest holding the switch was written. With curation
486
+ * on there is nothing to drain, and with no manifest written the drain does not run, so a
487
+ * line that says it was not drained for want of the root would name the wrong cause.
488
+ */
489
+ export async function lookupDrainLedgerRoot(opts, lookup = lookupLedgerRoot) {
490
+ if (opts.curationEnabled || opts.gitRoot === null || !opts.manifestWritten)
491
+ return { ledgerRoot: null, warning: null };
492
+ const own = await lookupOwnLedgerRoot(opts.gitRoot, lookup);
493
+ const warning = own.timedOut
494
+ ? `The curation queue was not drained for ${describeMoveText(opts.gitRoot)}: git did not answer in time. The next init tries again.`
495
+ : null;
496
+ return { ledgerRoot: own.root, warning };
497
+ }
498
+ /**
499
+ * Drain this repo's memory and curation queues for each feature init switched
432
500
  * off, so stale turns are not processed on a future re-enable — the same drains
433
- * `devflow memory --disable` and `devflow learning --disable` perform. Other
501
+ * `devflow memory --disable` and `devflow curation --disable` perform (the curation
502
+ * drain also removes the pending rows of the backups a move left). Other
434
503
  * repos' queues are inert: every gate reads the machine-wide switch, so nothing
435
504
  * appends to or processes them while the feature is off.
436
505
  *
@@ -443,13 +512,18 @@ export async function persistManifestThenConvergeTracker(opts) {
443
512
  * everywhere, so its queue is live and is left alone.
444
513
  *
445
514
  * D-LEDGER-MAIN-WORKTREE: each queue drains where the hooks write it — memory at
446
- * this checkout's toplevel (`gitRoot`), learning at the ledger root (`ledgerRoot`,
447
- * getLedgerRoot), which in a linked worktree is the main checkout.
515
+ * this checkout's toplevel (`gitRoot`), curation at the ledger root (`ledgerRoot`,
516
+ * lookupOwnLedgerRoot), which in a linked worktree is the main checkout.
448
517
  *
449
518
  * Returns one warning for each drain refused because a folder on the way to its
450
- * queue is a symbolic link (D-CLI-NO-SYMLINK); the caller prints them.
519
+ * queue is a symbolic link (D-CLI-NO-SYMLINK), one for a curation drain that
520
+ * could not list the backups (the live queue is drained then), and one for a curation
521
+ * drain that left the project alone because git tracks a data folder or did not answer
522
+ * (the fixed notice of a skipped move); the caller prints them. When the move step has
523
+ * printed that notice for the ledger root (`shownSkip`, its reason), the drain's line
524
+ * does not repeat it.
451
525
  */
452
- export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainLearningQueue }) {
526
+ export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainCurationQueue: drainDisabledCurationQueue }) {
453
527
  if (!opts.manifestWritten)
454
528
  return [];
455
529
  const refused = [];
@@ -458,10 +532,12 @@ export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue,
458
532
  if (!memory.drained)
459
533
  refused.push(formatRefusedDrain('memory', memory.linkedFolder));
460
534
  }
461
- if (!opts.learningEnabled && opts.ledgerRoot !== null) {
462
- const learning = await io.drainLearningQueue(opts.ledgerRoot);
463
- if (!learning.drained)
464
- refused.push(formatRefusedDrain('learning', learning.linkedFolder));
535
+ if (!opts.curationEnabled && opts.ledgerRoot !== null) {
536
+ const curation = await io.drainCurationQueue(opts.ledgerRoot);
537
+ if (!curation.drained)
538
+ refused.push(formatCurationNotDrained(curation, opts.shownSkip ?? null));
539
+ else if (curation.warning !== undefined)
540
+ refused.push(curation.warning);
465
541
  }
466
542
  return refused;
467
543
  }
@@ -472,18 +548,23 @@ export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue,
472
548
  * nothing else, so the manifest keeps every recorded value — plugins, version,
473
549
  * installedAt and every feature — and only `features.hud` turns on. The
474
550
  * earlier shape rewrote the whole record as a HUD-only fresh install (ambient,
475
- * memory, learning, knowledge, rules and proxy all `false`, plugins `[]`) while
551
+ * memory, curation, knowledge, rules and proxy all `false`, plugins `[]`) while
476
552
  * leaving those features' artifacts on disk: the record stopped describing the
477
553
  * machine, the next re-init seeded every feature off, and with
478
- * memory/learning/knowledge switched by the manifest alone
554
+ * memory/curation/knowledge switched by the manifest alone
479
555
  * (D-FEATURES-NARROW-ONLY) a HUD install would have really disabled them
480
556
  * everywhere. `version` is kept too: --hud-only reinstalls no plugin, and a
481
557
  * bumped version would make the next init skip the upgrade it still owes.
482
558
  *
483
559
  * With no prior manifest the result is the fresh HUD-only record: every other
484
- * feature off, which is what is installed.
560
+ * feature off, which is what is installed. `readManifest()` also reads a manifest it
561
+ * refuses as no manifest, so `refusedGateLevel` carries the machine gate level found in
562
+ * the raw file (`readStoredGateLevel`): the fresh record keeps it exactly as stored,
563
+ * malformed and `null` included, and holds no key when it is `undefined`. Init never
564
+ * writes a gate level the user did not choose. It is ignored over a prior install,
565
+ * whose own level is already kept.
485
566
  */
486
- export function buildHudOnlyManifest(existing, version, now) {
567
+ export function buildHudOnlyManifest(existing, version, now, refusedGateLevel) {
487
568
  if (existing !== null) {
488
569
  return {
489
570
  ...existing,
@@ -497,12 +578,13 @@ export function buildHudOnlyManifest(existing, version, now) {
497
578
  scope: 'user',
498
579
  features: {
499
580
  ambient: false, memory: false, hud: true, knowledge: false,
500
- learning: false, rules: false, flags: {}, proxy: false,
581
+ curation: false, rules: false, flags: {}, proxy: false,
501
582
  behavior: false,
502
583
  compliance: { enabled: false, frameworks: [] },
503
584
  // The exported default, not a literal — the one constant every other
504
585
  // module reads, so a moved default moves here too.
505
586
  tracker: { provider: DEFAULT_TRACKER_PROVIDER },
587
+ ...(refusedGateLevel !== undefined ? { gateLevel: refusedGateLevel } : {}),
506
588
  },
507
589
  installedAt: now,
508
590
  updatedAt: now,
@@ -613,8 +695,8 @@ export const initCommand = new Command('init')
613
695
  .option('--no-hud', 'Disable HUD status line')
614
696
  .option('--knowledge', 'Enable feature knowledge bases')
615
697
  .option('--no-knowledge', 'Disable feature knowledge bases')
616
- .option('--learning', 'Enable learning (decision/pitfall tracking)')
617
- .option('--no-learning', 'Disable learning (decision/pitfall tracking)')
698
+ .option('--curation', 'Enable curation (decision/pitfall tracking)')
699
+ .option('--no-curation', 'Disable curation (decision/pitfall tracking)')
618
700
  .option('--rules', 'Enable rules (always-on engineering principles)')
619
701
  .option('--no-rules', 'Disable rules')
620
702
  .option('--proxy', 'Enable external model routing (GPT models via your OpenAI/Codex subscription)')
@@ -624,6 +706,7 @@ export const initCommand = new Command('init')
624
706
  .option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
625
707
  .option('--no-compliance', 'Disable compliance (removes the rule; the skill and framework references stay installed; frameworks remembered for re-enable)')
626
708
  .option('--tracker <id>', 'The machine\'s default issue tracker provider: github, jira, or linear')
709
+ .option('--gate-level <full|standard|lean>', 'The machine\'s default gate level: full, standard, or lean (skips the gate level prompt)')
627
710
  .option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
628
711
  .option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
629
712
  .option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
@@ -649,6 +732,19 @@ export const initCommand = new Command('init')
649
732
  p.log.error('--reset and --plugin are mutually exclusive. Use --reset alone to restore defaults, or --plugin to update a specific plugin.');
650
733
  process.exit(1);
651
734
  }
735
+ // Parse --gate-level at the boundary, before anything is written (--hud-only writes early).
736
+ // Strict — reject, never repair — so a typo'd level exits here rather than being ignored.
737
+ let cliGateLevel;
738
+ {
739
+ const gateLevelResult = resolveGateLevelInitState(options.gateLevel, !!options.hudOnly);
740
+ if (gateLevelResult !== undefined) {
741
+ if (!gateLevelResult.ok) {
742
+ p.log.error(gateLevelResult.error);
743
+ process.exit(1);
744
+ }
745
+ cliGateLevel = gateLevelResult.value;
746
+ }
747
+ }
652
748
  // D-SCOPE-RETIRED: refuse a retired --scope value before anything is written.
653
749
  const scopeDecision = resolveRetiredScopeOption(options.scope);
654
750
  if (scopeDecision.kind === 'refuse') {
@@ -700,10 +796,25 @@ export const initCommand = new Command('init')
700
796
  existingHudManifest = await readManifest(devflowDir);
701
797
  }
702
798
  catch { /* absent on fresh install — existingHudManifest stays null */ }
799
+ // A manifest readManifest() refuses reads as none, but its machine gate level is still
800
+ // stored there: --hud-only keeps it and says so, as the full init does. --reset does not
801
+ // apply here, so nothing asks to forget it.
802
+ const hudStoredGate = await readStoredGateLevel(devflowDir, existingHudManifest, false);
803
+ let hudManifestWritten = false;
703
804
  try {
704
- await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, new Date().toISOString()));
805
+ await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, new Date().toISOString(), hudStoredGate.value));
806
+ hudManifestWritten = true;
705
807
  }
706
808
  catch { /* non-fatal */ }
809
+ if (hudStoredGate.origin !== 'manifest') {
810
+ logSummaryLines(formatGateLevelSummary({
811
+ level: hudStoredGate.value,
812
+ priorLevel: hudStoredGate.value,
813
+ priorOrigin: hudStoredGate.origin,
814
+ kept: true,
815
+ manifestWritten: hudManifestWritten,
816
+ }));
817
+ }
707
818
  const hudOnlySkew = formatHudOnlySkew(existingHudManifest?.version ?? null, await readRunningVersion());
708
819
  if (hudOnlySkew !== null)
709
820
  p.log.warn(hudOnlySkew);
@@ -718,6 +829,15 @@ export const initCommand = new Command('init')
718
829
  existingManifest = await readManifest(devflowDir, { onWarning: (message) => p.log.warn(message) });
719
830
  }
720
831
  catch { /* unreadable manifest — seeded as a fresh install */ }
832
+ // The stored machine gate level, and where it was found. readManifest() reads a manifest it
833
+ // refuses (a missing required field, a byte-order mark, unparseable JSON) as no manifest, so
834
+ // the seed holds no level for it, yet `devflow gates` and the settings resolver still read
835
+ // that file's level. Init never writes a level the user did not choose, so when readManifest()
836
+ // returned none the raw file decides: a level found in it is the level the run replaces, and
837
+ // is kept as stored unless --reset asks to forget it. A file that cannot be read at all is
838
+ // reported as "unreadable". This is an init-side override; the seed itself still comes from
839
+ // the manifest alone.
840
+ const storedGate = await readStoredGateLevel(devflowDir, existingManifest, !!options.reset);
721
841
  // D-INIT-NOT-HOME (same-location.ts): a repository rooted at HOME is no
722
842
  // project, so init treats it as no repository — no .devflow/config.json (that
723
843
  // would be the machine root's), no .claudeignore, no .gitignore block, no
@@ -935,7 +1055,7 @@ export const initCommand = new Command('init')
935
1055
  let memoryEnabled = seed.features.memory;
936
1056
  let hudEnabled = seed.features.hud;
937
1057
  let knowledgeEnabled = seed.features.knowledge;
938
- let learningEnabled = seed.features.learning;
1058
+ let curationEnabled = seed.features.curation;
939
1059
  let rulesEnabled = seed.features.rules;
940
1060
  // proxy: Advanced-only; Recommended path carries seed value unchanged.
941
1061
  // Fresh installs → false (FEATURE_DEFAULTS.proxy). Re-inits → prior manifest value.
@@ -956,11 +1076,21 @@ export const initCommand = new Command('init')
956
1076
  // alone, so init carries the prior value through untouched: it stamps the installed agents
957
1077
  // with it and writes it back to the manifest. Absent stays absent (the default list).
958
1078
  const trackerServers = seed.features.tracker.servers;
959
- // The machine gate level (features.gateLevel) is set by `devflow gates --set` alone, so init
960
- // carries the prior value through raw: no default, no narrowing, a stored `null` kept. The
961
- // seed holds `undefined` only for an absent key or a --reset seed, and the full manifest
962
- // write below then leaves the key out.
963
- const gateLevel = seed.features.gateLevel;
1079
+ // The machine gate level (features.gateLevel), shared with `devflow gates --set`. The seed holds
1080
+ // it raw: `undefined` only for an absent key or a --reset seed, a malformed value or `null` as
1081
+ // stored. When readManifest() returned none, the raw level (`storedGate`, read above) stands in
1082
+ // for the seed, as `carriedGateLevel` decides. It stays raw unless --gate-level or the wizard
1083
+ // step sets it, so a plain init never defaults, narrows or heals it; the full manifest write
1084
+ // below then leaves an absent key out. Each wizard path assigns the decision `runGateLevelStepAt`
1085
+ // makes; that is the only precedence site (--gate-level, then the wizard answer, then this
1086
+ // carried level), and `carried` says whether the level is still the carried one.
1087
+ const carriedGate = carriedGateLevel({
1088
+ manifest: existingManifest,
1089
+ reset: !!options.reset,
1090
+ stored: storedGate,
1091
+ seedLevel: seed.features.gateLevel,
1092
+ });
1093
+ let gateDecision = { level: carriedGate.value, carried: true };
964
1094
  let enabledFlags = { ...seed.flags };
965
1095
  // viewModeExplicit: true when --reset is passed; signals resolveFinalViewMode to let the
966
1096
  // seed-time view-mode win over an externally-set value in settings.json.
@@ -1018,6 +1148,31 @@ export const initCommand = new Command('init')
1018
1148
  }
1019
1149
  return trackerStep.state;
1020
1150
  };
1151
+ /**
1152
+ * Decide the machine gate level for one wizard path: --gate-level, else the wizard step
1153
+ * (gated on modePromptShown), else the stored value untouched. The step's outcome line is
1154
+ * the post-install gate level summary row, on both paths.
1155
+ *
1156
+ * Returns the decision, the level raw as `resolveGateLevelForInit` returns it and whether it
1157
+ * is the carried stored level. The caller assigns it to `gateDecision`: that value is the
1158
+ * only precedence decision, and nothing after the call revises it.
1159
+ */
1160
+ const runGateLevelStepAt = async (mode) => {
1161
+ const outcome = await resolveGateLevelForInit({
1162
+ cliLevel: cliGateLevel,
1163
+ stored: gateDecision.level,
1164
+ storedOrigin: carriedGate.origin,
1165
+ mode,
1166
+ modePromptShown,
1167
+ isTTY: process.stdin.isTTY,
1168
+ prompts: buildClackGateLevelPrompts(),
1169
+ });
1170
+ if (outcome.kind === 'cancelled') {
1171
+ p.cancel('Installation cancelled.');
1172
+ process.exit(0);
1173
+ }
1174
+ return { level: outcome.level, carried: outcome.carried };
1175
+ };
1021
1176
  // Safe-delete detection (both paths need this)
1022
1177
  const platform = detectPlatform();
1023
1178
  const shell = detectShell();
@@ -1057,6 +1212,9 @@ export const initCommand = new Command('init')
1057
1212
  // Tracker line via formatTrackerSummary, so the step's own outcome line
1058
1213
  // would be a duplicate.
1059
1214
  const wizardTracker = await runTrackerStepAt('recommended', seed.features.tracker.provider, false);
1215
+ // Gate level wizard step — same gate; --recommended and !isTTY keep the stored value.
1216
+ // The decision is final here: applyCliToggles below carries no gate level.
1217
+ gateDecision = await runGateLevelStepAt('recommended');
1060
1218
  // No attribution step here: the suppress-attribution question is Advanced-only (D27).
1061
1219
  // Recommended silently carries the seeded value in enabledFlags — fresh installs get
1062
1220
  // the registry default (off), re-inits get prior state. See shouldRunAttributionStep.
@@ -1068,7 +1226,7 @@ export const initCommand = new Command('init')
1068
1226
  memory: options.memory,
1069
1227
  hud: options.hud,
1070
1228
  knowledge: options.knowledge,
1071
- learning: options.learning,
1229
+ curation: options.curation,
1072
1230
  rules: options.rules,
1073
1231
  proxy: options.proxy,
1074
1232
  behavior: options.behavior,
@@ -1080,7 +1238,7 @@ export const initCommand = new Command('init')
1080
1238
  memoryEnabled = effectiveFeatures.memory;
1081
1239
  hudEnabled = effectiveFeatures.hud;
1082
1240
  knowledgeEnabled = effectiveFeatures.knowledge;
1083
- learningEnabled = effectiveFeatures.learning;
1241
+ curationEnabled = effectiveFeatures.curation;
1084
1242
  rulesEnabled = effectiveFeatures.rules;
1085
1243
  proxyEnabled = effectiveFeatures.proxy;
1086
1244
  behaviorEnabled = effectiveFeatures.behavior;
@@ -1119,7 +1277,7 @@ export const initCommand = new Command('init')
1119
1277
  const summaryLines = [
1120
1278
  `Ambient mode: ${ambientEnabled ? 'enabled' : 'disabled'}`,
1121
1279
  `Working memory: ${memoryEnabled ? 'enabled' : 'disabled'}`,
1122
- `Learning: ${learningEnabled ? 'enabled' : 'disabled'}`,
1280
+ `Curation: ${curationEnabled ? 'enabled' : 'disabled'}`,
1123
1281
  `Rules: ${rulesEnabled ? 'enabled' : 'disabled'}`,
1124
1282
  `HUD: ${hudEnabled ? 'enabled' : 'disabled'}`,
1125
1283
  `Knowledge bases: ${knowledgeEnabled ? 'enabled' : 'disabled'}`,
@@ -1130,6 +1288,9 @@ export const initCommand = new Command('init')
1130
1288
  // tracker step's ONLY surface on this path — both surfaces or it is
1131
1289
  // invisible on one path.
1132
1290
  `Tracker: ${formatTrackerSummary(trackerProvider)}`,
1291
+ // The level this run will record, or the stored one it keeps; the summary after the
1292
+ // install reports what the run really recorded.
1293
+ `Gate level: ${formatGateLevelRow(gateDecision.level, carriedGate.origin)}`,
1133
1294
  `View mode: ${readViewMode(enabledFlags)}`,
1134
1295
  `Claude Code flags: ${defaultFlagCount} configured`,
1135
1296
  `.claudeignore: ${resolveClaudeignoreOutcome(claudeignorePresent)}`,
@@ -1216,22 +1377,22 @@ export const initCommand = new Command('init')
1216
1377
  }
1217
1378
  knowledgeEnabled = knowledgeChoice;
1218
1379
  }
1219
- if (options.learning !== undefined) {
1220
- learningEnabled = options.learning;
1380
+ if (options.curation !== undefined) {
1381
+ curationEnabled = options.curation;
1221
1382
  }
1222
1383
  else {
1223
1384
  p.note('Detects architectural decisions and pitfalls from your session\n' +
1224
1385
  'dialogs. Runs a background agent on session stop that consumes\n' +
1225
- 'additional tokens. Applies to every project.', 'Learning (Decision/Pitfall Tracking)');
1226
- const learningChoice = await p.confirm({
1227
- message: 'Enable learning? (Recommended)',
1228
- initialValue: seed.features.learning,
1386
+ 'additional tokens. Applies to every project.', 'Curation (Decision/Pitfall Tracking)');
1387
+ const curationChoice = await p.confirm({
1388
+ message: 'Enable curation? (Recommended)',
1389
+ initialValue: seed.features.curation,
1229
1390
  });
1230
- if (p.isCancel(learningChoice)) {
1391
+ if (p.isCancel(curationChoice)) {
1231
1392
  p.cancel('Installation cancelled.');
1232
1393
  process.exit(0);
1233
1394
  }
1234
- learningEnabled = learningChoice;
1395
+ curationEnabled = curationChoice;
1235
1396
  }
1236
1397
  if (options.rules !== undefined) {
1237
1398
  rulesEnabled = options.rules;
@@ -1361,6 +1522,8 @@ export const initCommand = new Command('init')
1361
1522
  else
1362
1523
  p.log.info(overrideLine.text);
1363
1524
  }
1525
+ // Gate level wizard step — same gate as the Recommended path; re-init lands here.
1526
+ gateDecision = await runGateLevelStepAt('advanced');
1364
1527
  // Attribution feature (after compliance, before flags). This is the ONLY call site —
1365
1528
  // the attribution question is Advanced-only (D27); the Recommended path never asks and
1366
1529
  // silently carries the seeded value. The gate stays an explicit predicate call so the
@@ -1645,7 +1808,7 @@ export const initCommand = new Command('init')
1645
1808
  // The SETTLED switch, after the flag, the prompt and the seed have all had
1646
1809
  // their say (D-LEARNING-VARIANT-INSTALL): the files installed here and the
1647
1810
  // manifest written at the end of init cannot disagree about it.
1648
- learning: learningEnabled,
1811
+ curation: curationEnabled,
1649
1812
  trackerServers,
1650
1813
  // Non-fatal install notices with no other channel (skipped symlinks in the
1651
1814
  // generated reference tree, mode-normalisation failures) reach the user rather
@@ -1658,7 +1821,7 @@ export const initCommand = new Command('init')
1658
1821
  p.log.error(`${error}`);
1659
1822
  process.exit(1);
1660
1823
  }
1661
- // D-LEARNING-VARIANT-INSTALL: converge the learning variants. The file copy above
1824
+ // D-LEARNING-VARIANT-INSTALL: converge the curation variants. The file copy above
1662
1825
  // installed the variant of everything IT installed; on a `--plugin` install the
1663
1826
  // other plugins' files, already on disk, may still be the other variant (this run
1664
1827
  // may have flipped the switch), and the apply-decisions skill is removed here when
@@ -1666,21 +1829,21 @@ export const initCommand = new Command('init')
1666
1829
  // Warn-not-abort: either mismatch is safe (an on variant is still gated at run
1667
1830
  // time, an off variant loads no decisions). The agent mapping is reapplied below.
1668
1831
  try {
1669
- const learningConverge = await convergeLearningVariants({
1832
+ const curationConverge = await convergeCurationVariants({
1670
1833
  claudeDir,
1671
1834
  devflowDir,
1672
- learning: learningEnabled,
1835
+ curation: curationEnabled,
1673
1836
  plugins: effectivePlugins,
1674
1837
  warn: (msg) => installWarnings.push(msg),
1675
1838
  });
1676
- if (verbose && learningConverge.converged) {
1677
- p.log.info(`Learning ${learningEnabled ? 'on' : 'off'} variants: ` +
1678
- `${learningConverge.commandsRewritten.length} command(s) and ${learningConverge.agentsRewritten.length} agent(s) ` +
1679
- `rewritten, ${learningConverge.unchanged} already current`);
1839
+ if (verbose && curationConverge.converged) {
1840
+ p.log.info(`Curation ${curationEnabled ? 'on' : 'off'} variants: ` +
1841
+ `${curationConverge.commandsRewritten.length} command(s) and ${curationConverge.agentsRewritten.length} agent(s) ` +
1842
+ `rewritten, ${curationConverge.unchanged} already current`);
1680
1843
  }
1681
1844
  }
1682
1845
  catch (err) {
1683
- installWarnings.push(`Learning variant convergence failed — ${err instanceof Error ? err.message : String(err)}`);
1846
+ installWarnings.push(`Curation variant convergence failed — ${err instanceof Error ? err.message : String(err)}`);
1684
1847
  }
1685
1848
  // Converge compliance artifacts (always converge, never short-circuit).
1686
1849
  // Called unconditionally so that enabling/disabling compliance during init
@@ -1826,6 +1989,8 @@ export const initCommand = new Command('init')
1826
1989
  'lib/staleness.cjs',
1827
1990
  'dream-lock',
1828
1991
  'decisions-usage-scan.cjs',
1992
+ 'learning-lock',
1993
+ 'lib/learning-store.cjs',
1829
1994
  ];
1830
1995
  const hooksDir = path.join(devflowDir, 'scripts', 'hooks');
1831
1996
  for (const legacy of LEGACY_HOOK_FILES) {
@@ -1835,6 +2000,10 @@ export const initCommand = new Command('init')
1835
2000
  }
1836
2001
  catch { /* doesn't exist */ }
1837
2002
  }
2003
+ // The curation move (#529): after the legacy sweep, before the manifest persist and
2004
+ // the disabled-feature drain below, so the drain sees the moved folder. Its lines
2005
+ // and its one notice print with the install warnings, after the spinner stops.
2006
+ const curationMove = await moveCurationData({ discoveredProjects, gitRoot, homeDir, devflowDir });
1838
2007
  // === Settings & hooks (all automatic based on collected choices) ===
1839
2008
  s.message('Configuring settings');
1840
2009
  await installSettings(claudeDir, rootDir, devflowDir, verbose);
@@ -2061,7 +2230,7 @@ export const initCommand = new Command('init')
2061
2230
  'Manifest records intended state; run devflow init again to retry.');
2062
2231
  }
2063
2232
  // Write .devflow/config.json — facts about this repo. The machine switches for
2064
- // memory/learning/knowledge are the manifest's; a hand-written `features`
2233
+ // memory/curation/knowledge are the manifest's; a hand-written `features`
2065
2234
  // object here only narrows them and is carried, never written
2066
2235
  // (D-FEATURES-NARROW-ONLY; the managed write drops the retired top-level
2067
2236
  // per-repo keys). A managed
@@ -2278,6 +2447,10 @@ export const initCommand = new Command('init')
2278
2447
  logSummaryLines(formatSkillScopeSummary(installReport, pluginListUnchanged));
2279
2448
  for (const warning of installWarnings)
2280
2449
  p.log.warn(warning);
2450
+ for (const line of curationMove.lines)
2451
+ p.log.warn(describeMoveText(line));
2452
+ if (curationMove.notice !== null)
2453
+ p.log.info(curationMove.notice);
2281
2454
  const installedSet = new Set(pluginsToInstall.flatMap(p => p.commands).filter(c => c.length > 0));
2282
2455
  const orderedCommands = WORKFLOW_ORDER.filter(cmd => installedSet.has(cmd));
2283
2456
  if (orderedCommands.length > 0) {
@@ -2325,7 +2498,7 @@ export const initCommand = new Command('init')
2325
2498
  memory: memoryEnabled,
2326
2499
  hud: hudEnabled,
2327
2500
  knowledge: knowledgeEnabled,
2328
- learning: learningEnabled,
2501
+ curation: curationEnabled,
2329
2502
  rules: rulesEnabled,
2330
2503
  // FlagsRecord written directly — key-presence encodes "known".
2331
2504
  // view-mode is encoded as flags['view-mode'] (the resolved final value).
@@ -2345,8 +2518,9 @@ export const initCommand = new Command('init')
2345
2518
  // through parseTrackerId (CLI), the typed wizard select, or the seed, which
2346
2519
  // itself came through normalizeTrackerFeature on read.
2347
2520
  tracker: { provider: trackerProvider, ...(trackerServers !== undefined ? { servers: trackerServers } : {}) },
2348
- // The prior machine gate level, raw (see where `gateLevel` is read above).
2349
- ...(gateLevel !== undefined ? { gateLevel } : {}),
2521
+ // The machine gate level: --gate-level, the wizard answer, or the prior value raw
2522
+ // (see where `gateDecision` is read above).
2523
+ ...(gateDecision.level !== undefined ? { gateLevel: gateDecision.level } : {}),
2350
2524
  },
2351
2525
  installedAt: existingManifest?.installedAt ?? now,
2352
2526
  updatedAt: now,
@@ -2368,12 +2542,16 @@ export const initCommand = new Command('init')
2368
2542
  p.log.info(msg.text);
2369
2543
  }
2370
2544
  // Only now that the machine-wide switch is on disk (D-INIT-DRAIN-AFTER-SWITCH).
2545
+ const drainRoot = await lookupDrainLedgerRoot({ gitRoot, curationEnabled, manifestWritten: trackerLifecycle.manifestWritten });
2546
+ if (drainRoot.warning !== null)
2547
+ p.log.warn(drainRoot.warning);
2371
2548
  const refusedDrains = await drainDisabledFeatureQueues({
2372
2549
  gitRoot,
2373
- ledgerRoot: learningEnabled || gitRoot === null ? null : await getLedgerRoot(),
2550
+ ledgerRoot: drainRoot.ledgerRoot,
2374
2551
  memoryEnabled,
2375
- learningEnabled,
2552
+ curationEnabled,
2376
2553
  manifestWritten: trackerLifecycle.manifestWritten,
2554
+ shownSkip: shownSkipAt(curationMove, drainRoot.ledgerRoot),
2377
2555
  });
2378
2556
  for (const line of refusedDrains)
2379
2557
  p.log.warn(line);
@@ -2401,6 +2579,14 @@ export const initCommand = new Command('init')
2401
2579
  agent: trackerLifecycle.agent,
2402
2580
  });
2403
2581
  logSummaryLines(trackerLines);
2582
+ // The gate level this run recorded, from the manifest the run wrote (D-INIT-REAL-OUTCOME).
2583
+ logSummaryLines(formatGateLevelSummary({
2584
+ level: gateDecision.level,
2585
+ priorLevel: storedGate.value,
2586
+ priorOrigin: storedGate.origin,
2587
+ kept: gateDecision.carried && carriedGate.origin === 'rejected',
2588
+ manifestWritten: trackerLifecycle.manifestWritten,
2589
+ }));
2404
2590
  // CLAUDE.md import audit (D-CLAUDE-MD-IMPORT-AUDIT, D-AUDIT-STAMP): after the install, on
2405
2591
  // both paths, through claudeMdAuditStep. Global roots always; the project's roots when init
2406
2592
  // runs inside a git repository that is not HOME, at its toplevel. The stamp is written here,
@@ -40,7 +40,7 @@ const MEMORY_HOOK_CONFIG = {
40
40
  *
41
41
  * D-EXACT-HOOK-OWNER (hooks.ts): matched as command ENDINGS under any directory —
42
42
  * 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
43
+ * prompt-capture, curation, decisions, knowledge-refresh, sidecar and dream
44
44
  * pipelines — never as substrings, so a user's hook that mentions one is theirs.
45
45
  */
46
46
  const LEGACY_HOOK_SUFFIXES = {
@@ -165,7 +165,7 @@ export function convergeMemoryHooks(settingsJson, enabled, devflowDir) {
165
165
  * --no-memory` and `devflow memory --disable`. Refused, deleting nothing, when
166
166
  * `.devflow` or `.devflow/memory` under `projectRoot` is a symbolic link
167
167
  * (D-CLI-NO-SYMLINK). ENOENT-tolerant; any other error propagates to the command
168
- * boundary, like drainLearningQueue.
168
+ * boundary, like drainCurationQueue.
169
169
  */
170
170
  export async function drainMemoryQueue(projectRoot) {
171
171
  const memoryDir = getMemoryDir(projectRoot);
@@ -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) {