devflow-kit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Rendering for the post-install summary — pure functions that turn an
3
+ * {@link InstallReport} into lines, logging nothing (applies ADR-013).
4
+ *
5
+ * Its own module rather than a section of init.ts because `devflow tracker --set`
6
+ * renders the same overlay outcomes from a different command. A CLI command
7
+ * importing a renderer out of a 2,450-line sibling couples two commands through
8
+ * a file neither of them owns; both import from here instead (design review M5).
9
+ */
10
+ import color from 'picocolors';
11
+ import { overlayUnitLabel } from '../../targets/claude-code/installer.js';
12
+ import { SKILL_REFS_SKILL_NAME } from '../../core/mds-variants.js';
13
+ import { prefixSkillName } from '../../core/plugins.js';
14
+ /**
15
+ * Turn the reference-overlay half of an InstallReport into summary lines.
16
+ *
17
+ * The overlay rewrites files inside an installed skill directory the user may have
18
+ * shadowed, and a unit it could not refresh is left in one of the states
19
+ * {@link OverlayFailureState} enumerates — running on the previous install, half
20
+ * replaced, absent, or recoverable only from a backup path. None of that is visible from
21
+ * the filesystem at a glance, so all of it reaches the summary — PF-015: a report field
22
+ * with no render site is not a report, and a render site that flattens four states into
23
+ * one sentence is the same defect one layer up.
24
+ *
25
+ * Pure function — returns lines, logs nothing (applies ADR-013).
26
+ *
27
+ * @param provider - The resolved tracker provider the overlay converged to. The
28
+ * count alone cannot say WHICH mechanics are installed, and after the install
29
+ * became selection-scoped that is the number's whole meaning.
30
+ * @param skillName - Bare name of the skill hosting the generated references,
31
+ * rendered `devflow:`-prefixed. Defaults to the core constant the build path and
32
+ * the installer's overlay trigger both read, so the renderer is never a third
33
+ * independent statement of which skill owns them — the divergence PF-013
34
+ * describes, where changing the answer means finding every retyped spelling and
35
+ * nothing fails if one is missed.
36
+ */
37
+ export function formatOverlaySummary(report, provider, skillName = SKILL_REFS_SKILL_NAME) {
38
+ const lines = [];
39
+ if (report.overlaidRefs.length > 0) {
40
+ const scope = provider === undefined ? '' : ` (${provider} tracker mechanics)`;
41
+ lines.push({
42
+ level: 'info',
43
+ message: `Installed ${report.overlaidRefs.length} generated skill reference(s) for ` +
44
+ prefixSkillName(skillName) + scope,
45
+ });
46
+ }
47
+ for (const failure of report.overlayFailures) {
48
+ lines.push({
49
+ level: 'warn',
50
+ message: `Could not refresh the generated references for ${overlayUnitLabel(failure.unit)} ` +
51
+ `(${failure.error}) — ${describeOverlayFailureState(failure.state)}`,
52
+ });
53
+ }
54
+ return lines;
55
+ }
56
+ /**
57
+ * The half of an overlay warning that describes what is actually on disk.
58
+ *
59
+ * One sentence per state, each true of that state and of no other. A single shared
60
+ * sentence — "the previously installed files were left unchanged" — is true of the first
61
+ * arm only, and would read loudest over the arms it fits worst: a set left
62
+ * half-refreshed, and a unit whose only surviving copy is a backup path the user has to
63
+ * be told about.
64
+ *
65
+ * Exported because `devflow tracker --set` renders the same states when it aborts
66
+ * on an overlay failure (applies PF-013 — one sentence per state, in one place,
67
+ * rather than a second wording that drifts).
68
+ *
69
+ * Exhaustive over {@link OverlayFailureState} — a new state added to the union without a
70
+ * sentence here is a compile error, not a state that silently prints nothing.
71
+ */
72
+ export function describeOverlayFailureState(state) {
73
+ switch (state.kind) {
74
+ case 'installed-unchanged':
75
+ return 'the previously installed files were left unchanged';
76
+ case 'not-installed':
77
+ return (`nothing is installed in their place, so ${state.absent.length} reference(s) the ` +
78
+ `agent is told to load are absent: ${state.absent.join(', ')}`);
79
+ case 'partially-refreshed':
80
+ return (`${state.refreshed.length} of ${state.refreshed.length + state.stale.length} ` +
81
+ `document(s) had already been replaced, so the set is part new and part old — ` +
82
+ `still on the previous install: ${state.stale.join(', ') || 'none'}`);
83
+ case 'restore-failed':
84
+ return (`the displaced copy could NOT be put back (${state.restoreError}), so nothing is ` +
85
+ `installed there now — the only surviving copy is "${state.recoveryPath}", which ` +
86
+ `this run's stale-reference prune was skipped to preserve`);
87
+ default: {
88
+ const _exhaustive = state;
89
+ void _exhaustive;
90
+ return 'the state it was left in is unknown';
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * The install summary's tracker rows.
96
+ *
97
+ * Two facts the previous summary never stated, and after the install became
98
+ * selection-scoped both of them decide what the user actually has:
99
+ *
100
+ * - WHICH provider is active. The install has no other visible trace of it —
101
+ * the sentinel is a zero-byte dotfile and the mechanics are a directory the
102
+ * user has no reason to list. `(default)` distinguishes "github because I
103
+ * chose it" from "github because nothing was chosen"; `(was jira)` is what
104
+ * makes a self-heal or a `--reset` collapse legible rather than silent.
105
+ * - WHAT the provider change moved. A provider swap installs one tree and
106
+ * prunes another, and the agent file appears or disappears with it.
107
+ *
108
+ * The delta line is emitted only when something moved: on a steady-state re-init
109
+ * the counts are noise.
110
+ *
111
+ * Pure function — returns lines, logs nothing (applies ADR-013).
112
+ *
113
+ * @param previous - The provider recorded by the PRIOR manifest, or undefined on
114
+ * a first install. Rendered only when it differs from `provider`.
115
+ * @param isDefault - Whether `provider` is the registry default.
116
+ */
117
+ export function formatTrackerAssetSummary(input) {
118
+ const lines = [];
119
+ const changed = input.previous !== undefined && input.previous !== input.provider;
120
+ const qualifier = changed
121
+ ? ` ${color.dim(`(was ${input.previous})`)}`
122
+ : input.isDefault ? ` ${color.dim('(default)')}` : '';
123
+ lines.push({ level: 'info', message: `Tracker: ${input.provider}${qualifier}` });
124
+ const agentNote = input.agent === 'unchanged' ? '' : `, tracker agent ${input.agent}`;
125
+ if (input.installedRefs > 0 || input.removedRefs > 0 || agentNote !== '') {
126
+ lines.push({
127
+ level: 'info',
128
+ message: `Tracker assets: +${input.installedRefs} reference(s), ` +
129
+ `−${input.removedRefs} reference(s)${agentNote}`,
130
+ });
131
+ }
132
+ return lines;
133
+ }
134
+ /**
135
+ * Did this run's plugin selection match the one already on disk?
136
+ *
137
+ * The question {@link formatSkillScopeSummary}'s `pluginListUnchanged` asks, and
138
+ * a separate function because the two halves fail differently and only one of
139
+ * them had executed evidence (design review L2): the renderer's behaviour given
140
+ * an answer, and the answer itself.
141
+ *
142
+ * Two clauses, and the first one is the one a set comparison alone would lose:
143
+ *
144
+ * - **A prior manifest must EXIST.** `null` is a first install. Nothing was
145
+ * removed from a user who had nothing, so there is no upgrade to explain,
146
+ * and comparing "no previous selection" against this run's would otherwise
147
+ * read as a match whenever both are empty.
148
+ * - **The plugin SETS must be equal**, not the arrays: order is an artifact of
149
+ * how the selection was assembled, and a duplicate name in either list is a
150
+ * manifest detail rather than a different selection.
151
+ *
152
+ * Pure function (applies ADR-013).
153
+ *
154
+ * @param previousPlugins - `manifest.plugins` as it stands before this run, or
155
+ * `null` when there is no prior manifest.
156
+ * @param effectivePluginNames - What this run installs.
157
+ */
158
+ export function isPluginListUnchanged(previousPlugins, effectivePluginNames) {
159
+ if (previousPlugins === null)
160
+ return false;
161
+ const previous = new Set(previousPlugins);
162
+ const effective = new Set(effectivePluginNames);
163
+ return previous.size === effective.size && [...effective].every(name => previous.has(name));
164
+ }
165
+ /**
166
+ * Turn the skill-scoping half of an InstallReport into summary lines.
167
+ *
168
+ * Two facts the filesystem cannot tell the user apart from an install that never
169
+ * happened:
170
+ *
171
+ * - a REMOVED skill. Scoping the install means a re-init after deselecting a
172
+ * plugin silently deletes skills the previous install carried. The line is
173
+ * emitted only when the plugin list is UNCHANGED, because that is the case
174
+ * the user did not ask for: they re-ran init expecting nothing to move, and
175
+ * the scoping change is what moved it. When they deselected a plugin
176
+ * themselves, the removal is the thing they asked for and needs no notice.
177
+ * - a DORMANT shadow. `~/.devflow/skills/{name}/` is never deleted, so an
178
+ * inactive shadow and an applied one look identical from disk.
179
+ *
180
+ * `pluginListUnchanged` is the caller's answer to "did the selection move?", and
181
+ * it is FALSE when there is no prior manifest: a first install removed nothing a
182
+ * user had, so there is no upgrade to explain (design review L2).
183
+ *
184
+ * Pure function — returns lines, logs nothing (applies ADR-013).
185
+ */
186
+ export function formatSkillScopeSummary(report, pluginListUnchanged) {
187
+ const lines = [];
188
+ if (pluginListUnchanged && report.removedSkills.length > 0) {
189
+ const names = [...report.removedSkills].sort().join(', ');
190
+ lines.push({
191
+ level: 'info',
192
+ message: `Removed ${report.removedSkills.length} skill(s) no selected plugin requires: ${names}. ` +
193
+ `Re-run devflow init and select the plugin that provides them to keep them.`,
194
+ });
195
+ }
196
+ for (const name of report.dormantShadows) {
197
+ lines.push({
198
+ level: 'info',
199
+ message: `Shadow for ${name} is inactive — the plugin that uses it is not selected ` +
200
+ `(${color.dim('kept in ~/.devflow/skills/')})`,
201
+ });
202
+ }
203
+ return lines;
204
+ }
205
+ //# sourceMappingURL=install-report.js.map
@@ -10,8 +10,8 @@ import { handleToggle } from './toggle.js';
10
10
  import { handleList } from './list.js';
11
11
  export const knowledgeCommand = new Command('knowledge')
12
12
  .description('Manage per-feature knowledge bases')
13
- .option('--enable', 'Enable per-feature knowledge bases')
14
- .option('--disable', 'Disable per-feature knowledge bases')
13
+ .option('--enable', 'Enable per-feature knowledge bases in every project')
14
+ .option('--disable', 'Disable per-feature knowledge bases in every project')
15
15
  .option('--status', 'Show knowledge base feature status')
16
16
  .action(async (options) => {
17
17
  await handleToggle(options);
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Handle the enable/disable/status toggle actions for `devflow knowledge`.
3
3
  *
4
- * The sole opt-out mechanism is the feature config `knowledge` field (config-only gate per ADR-001).
4
+ * D-FEATURES-MACHINE-WIDE (src/core/feature-switch.ts): knowledge write-back is
5
+ * switched for the whole machine by `features.knowledge` in
6
+ * ~/.devflow/manifest.json. `--enable`/`--disable` write that value — the same
7
+ * one `devflow init --knowledge / --no-knowledge` writes — and `--status`
8
+ * reports it.
5
9
  */
6
10
  import { promises as fs } from 'fs';
7
11
  import * as path from 'path';
@@ -9,8 +13,7 @@ import * as p from '@clack/prompts';
9
13
  import color from 'picocolors';
10
14
  import { getGitRoot } from '../../../core/git.js';
11
15
  import { getDevFlowDirectory } from '../../../targets/claude-code/claude-paths.js';
12
- import { readManifest, writeManifest } from '../../../core/manifest.js';
13
- import { updateFeature, isFeatureEnabled } from '../../../core/feature-config.js';
16
+ import { readMachineFeature, writeMachineFeature } from '../../../core/feature-switch.js';
14
17
  import { getFeaturesDir } from '../../../core/project-paths.js';
15
18
  async function getWorktreePath() {
16
19
  return (await getGitRoot()) ?? process.cwd();
@@ -39,46 +42,33 @@ async function countKnowledgeBases(worktreePath) {
39
42
  export async function handleToggle(options) {
40
43
  if (!options.enable && !options.disable && !options.status)
41
44
  return;
42
- const worktreePath = await getWorktreePath();
43
45
  const devflowDir = getDevFlowDirectory();
44
- if (options.enable) {
45
- p.intro(color.cyan('Enable Feature Knowledge Bases'));
46
- // Update feature config (the sole gate — config-only per ADR-001)
47
- await updateFeature(worktreePath, 'knowledge', true);
48
- // Update manifest
49
- const manifest = await readManifest(devflowDir);
50
- if (manifest) {
51
- manifest.features.knowledge = true;
52
- manifest.updatedAt = new Date().toISOString();
53
- await writeManifest(devflowDir, manifest);
54
- }
55
- p.log.success('Feature knowledge bases enabled');
56
- p.log.info('Knowledge bases are created automatically when workflows detect documented area changes.');
46
+ if (options.status) {
47
+ p.intro(color.cyan('Feature Knowledge Status'));
48
+ const enabled = await readMachineFeature(devflowDir, 'knowledge');
49
+ const kbCount = await countKnowledgeBases(await getWorktreePath());
50
+ p.log.info(`Status: ${enabled ? color.green('enabled') : color.yellow('disabled')}`);
51
+ p.log.info(`Knowledge bases: ${kbCount}`);
57
52
  p.outro('');
53
+ return;
58
54
  }
59
- else if (options.disable) {
60
- p.intro(color.cyan('Disable Feature Knowledge Bases'));
61
- // Update feature config (the sole gate — config-only per ADR-001)
62
- await updateFeature(worktreePath, 'knowledge', false);
63
- // Update manifest
64
- const manifest = await readManifest(devflowDir);
65
- if (manifest) {
66
- manifest.features.knowledge = false;
67
- manifest.updatedAt = new Date().toISOString();
68
- await writeManifest(devflowDir, manifest);
69
- }
70
- p.log.success('Feature knowledge bases disabled');
71
- p.log.info('Existing knowledge bases preserved. Write-back skipped while disabled.');
55
+ const enabled = options.enable === true;
56
+ p.intro(color.cyan(`${enabled ? 'Enable' : 'Disable'} Feature Knowledge Bases`));
57
+ const recorded = await writeMachineFeature(devflowDir, 'knowledge', enabled);
58
+ if (!recorded.ok) {
59
+ p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
60
+ process.exitCode = 1;
72
61
  p.outro('');
62
+ return;
63
+ }
64
+ if (enabled) {
65
+ p.log.success('Feature knowledge bases enabled in every project');
66
+ p.log.info('Knowledge bases are created automatically when workflows detect documented area changes.');
73
67
  }
74
68
  else {
75
- // options.status
76
- p.intro(color.cyan('Feature Knowledge Status'));
77
- const enabled = await isFeatureEnabled(worktreePath, 'knowledge');
78
- const kbCount = await countKnowledgeBases(worktreePath);
79
- p.log.info(`Status: ${enabled ? color.green('enabled') : color.yellow('disabled')}`);
80
- p.log.info(`Knowledge bases: ${kbCount}`);
81
- p.outro('');
69
+ p.log.success('Feature knowledge bases disabled in every project');
70
+ p.log.info('Existing knowledge bases preserved. Write-back skipped while disabled.');
82
71
  }
72
+ p.outro('');
83
73
  }
84
74
  //# sourceMappingURL=toggle.js.map
@@ -4,8 +4,7 @@ import * as path from 'path';
4
4
  import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getLearningDir, getLearningTuningConfigPath, getDecisionsLogPath, getDecisionsLockDir, } from '../../core/project-paths.js';
7
- import { updateFeature, isFeatureEnabled } from '../../core/feature-config.js';
8
- import { syncManifestFeature } from '../../core/manifest.js';
7
+ import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
9
8
  import { getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
10
9
  import { getGitRoot } from '../../core/git.js';
11
10
  import { sweepLegacyDreamMarkers, drainLearningQueue } from '../../core/learning-queue-cleanup.js';
@@ -15,8 +14,8 @@ import { readObservations, warnIfInvalid, } from '../../core/observation-io.js';
15
14
  // ---------------------------------------------------------------------------
16
15
  function printUsage() {
17
16
  p.intro(color.bgCyan(color.black(' Learning ')));
18
- p.note(`${color.cyan('devflow learning --enable')} Enable learning (decision + pitfall detection)\n` +
19
- `${color.cyan('devflow learning --disable')} Disable learning (drains queue)\n` +
17
+ p.note(`${color.cyan('devflow learning --enable')} Enable learning in every project\n` +
18
+ `${color.cyan('devflow learning --disable')} Disable learning in every project (drains this project's queue)\n` +
20
19
  `${color.cyan('devflow learning --status')} Show learning status\n` +
21
20
  `${color.cyan('devflow learning --list')} Show all observations\n` +
22
21
  `${color.cyan('devflow learning --configure')} Configuration wizard\n` +
@@ -37,13 +36,16 @@ async function requireGitRoot(actionSuffix) {
37
36
  return gitRoot;
38
37
  }
39
38
  async function handleStatus() {
39
+ // D-FEATURES-MACHINE-WIDE: the one switch is the manifest's, so the state is
40
+ // the same from every directory; only the observation counts are per-project.
41
+ const enabled = await readMachineFeature(getDevFlowDirectory(), 'learning');
42
+ const stateLine = `Learning: ${enabled ? 'enabled' : 'disabled'}`;
40
43
  const gitRoot = await getGitRoot();
41
44
  if (!gitRoot) {
42
- p.log.info('Learning: disabled (not in a git project)');
45
+ p.log.info(`${stateLine}\nObservations: not in a git project`);
43
46
  return;
44
47
  }
45
48
  const logPath = getDecisionsLogPath(gitRoot);
46
- const enabled = await isFeatureEnabled(gitRoot, 'learning');
47
49
  const { observations, invalidCount } = await readObservations(logPath);
48
50
  const decisionObs = observations.filter(o => o.type === 'decision' || o.type === 'pitfall');
49
51
  const decisions = observations.filter(o => o.type === 'decision');
@@ -52,7 +54,7 @@ async function handleStatus() {
52
54
  const ready = decisionObs.filter(o => o.status === 'ready');
53
55
  const observing = decisionObs.filter(o => o.status === 'observing');
54
56
  const deprecated = decisionObs.filter(o => o.status === 'deprecated');
55
- const lines = [`Learning: ${enabled ? 'enabled' : 'disabled'}`];
57
+ const lines = [stateLine];
56
58
  if (decisionObs.length === 0) {
57
59
  lines.push('Observations: none');
58
60
  }
@@ -235,32 +237,37 @@ async function handleClear() {
235
237
  await drainLearningQueue(gitRoot);
236
238
  p.log.success('Decisions log cleared.');
237
239
  }
238
- async function handleEnable() {
239
- const gitRoot = await requireGitRoot('configuration not updated');
240
- if (!gitRoot)
240
+ /**
241
+ * `--enable` / `--disable`: the machine-wide switch (D-FEATURES-MACHINE-WIDE),
242
+ * converged exactly as `devflow init --learning / --no-learning` converges it —
243
+ * the manifest value, and on disable a drained queue in the current project.
244
+ * Never requires a git root: the switch is not a per-project setting.
245
+ */
246
+ async function handleToggle(enabled) {
247
+ const recorded = await writeMachineFeature(getDevFlowDirectory(), 'learning', enabled);
248
+ if (!recorded.ok) {
249
+ p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
250
+ process.exitCode = 1;
241
251
  return;
242
- await updateFeature(gitRoot, 'learning', true);
243
- await syncManifestFeature(getDevFlowDirectory(), 'learning', true);
244
- p.log.success('Learning enabled — configuration updated');
245
- p.log.info(color.dim('Architectural decisions and pitfalls will be detected from your sessions'));
246
- }
247
- async function handleDisable() {
248
- const gitRoot = await requireGitRoot('configuration not updated');
249
- if (!gitRoot)
252
+ }
253
+ if (enabled) {
254
+ p.log.success('Learning enabled in every project');
255
+ p.log.info(color.dim('Architectural decisions and pitfalls will be detected from your sessions'));
250
256
  return;
251
- await updateFeature(gitRoot, 'learning', false);
252
- // Drain the learning (decisions-detection) queue so stale turns don't process
253
- // on re-enable — mirrors memory.ts's drain-on-disable behavior for the
254
- // sibling memory queue. Unconditional: a mid-run Learning agent whose claimed
257
+ }
258
+ // Drain the current project's learning (decisions-detection) queue so stale
259
+ // turns don't process on re-enable. A mid-run Learning agent whose claimed
255
260
  // batch vanishes aborts without changes — the desired outcome of disabling.
256
- await drainLearningQueue(gitRoot);
257
- await syncManifestFeature(getDevFlowDirectory(), 'learning', false);
258
- p.log.success('Learning disabled — configuration updated');
261
+ const gitRoot = await getGitRoot();
262
+ if (gitRoot) {
263
+ await drainLearningQueue(gitRoot);
264
+ }
265
+ p.log.success('Learning disabled in every project');
259
266
  }
260
267
  export const learningCommand = new Command('learning')
261
- .description('Enable or disable learning (decision/pitfall detection + knowledge base)')
262
- .option('--enable', 'Enable learning')
263
- .option('--disable', 'Disable learning')
268
+ .description('Enable or disable learning (decision/pitfall detection) in every project')
269
+ .option('--enable', 'Enable learning in every project')
270
+ .option('--disable', 'Disable learning in every project')
264
271
  .option('--status', 'Show learning status and observation counts')
265
272
  .option('--list', 'Show all decision/pitfall observations sorted by confidence')
266
273
  .option('--configure', 'Interactive configuration wizard for learning.json')
@@ -300,11 +307,11 @@ export const learningCommand = new Command('learning')
300
307
  return;
301
308
  }
302
309
  if (options.enable) {
303
- await handleEnable();
310
+ await handleToggle(true);
304
311
  return;
305
312
  }
306
313
  if (options.disable) {
307
- await handleDisable();
314
+ await handleToggle(false);
308
315
  return;
309
316
  }
310
317
  });
@@ -4,12 +4,11 @@ import * as path from 'path';
4
4
  import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
7
- import { syncManifestFeature } from '../../core/manifest.js';
8
7
  import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
9
8
  import { discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
10
9
  import { getGitRoot } from '../../core/git.js';
11
10
  import { getMemoryDir, getPendingTurnsPath, getPendingTurnsProcessingPath, } from '../../core/project-paths.js';
12
- import { updateFeature, isFeatureEnabled } from '../../core/feature-config.js';
11
+ import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
13
12
  /**
14
13
  * Map of hook event type → filename marker for the memory hooks.
15
14
  * Three hooks total: Stop, SessionStart, PreCompact.
@@ -141,6 +140,39 @@ export function countMemoryHooks(input) {
141
140
  }
142
141
  return count;
143
142
  }
143
+ /**
144
+ * Converge the memory hooks in a settings JSON string to `enabled`. Pure.
145
+ *
146
+ * D-FEATURES-MACHINE-WIDE: the ONE settings transform for the memory feature,
147
+ * shared by `devflow init` (inside its single settings read-modify-write pass)
148
+ * and `devflow memory --enable/--disable`, so the two controls of the same
149
+ * machine-wide switch leave settings.json byte-for-byte alike. Always
150
+ * remove-then-add, which also upgrades an older hook format (e.g. `.sh` →
151
+ * `run-hook`) in place.
152
+ *
153
+ * Stop-array ordering (AC-C2): memory-worker is appended after whatever the
154
+ * Stop array already holds, so it lands after capture-turn as long as the
155
+ * capture hooks are registered first — init registers them earlier in the same
156
+ * pass, and on a standalone toggle they are already present.
157
+ */
158
+ export function convergeMemoryHooks(settingsJson, enabled, devflowDir) {
159
+ const cleaned = removeMemoryHooks(settingsJson);
160
+ return enabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
161
+ }
162
+ /**
163
+ * Drain a project's pending memory queue (and a claimed batch) so stale turns
164
+ * are not processed when memory is next switched on. Shared by `devflow init
165
+ * --no-memory` and `devflow memory --disable`. ENOENT-tolerant; any other
166
+ * error propagates to the command boundary, like drainLearningQueue.
167
+ */
168
+ export async function drainMemoryQueue(projectRoot) {
169
+ const ignoreMissing = (e) => { if (e.code !== 'ENOENT')
170
+ throw e; };
171
+ await Promise.all([
172
+ fs.unlink(getPendingTurnsPath(projectRoot)).catch(ignoreMissing),
173
+ fs.unlink(getPendingTurnsProcessingPath(projectRoot)).catch(ignoreMissing),
174
+ ]);
175
+ }
144
176
  /**
145
177
  * Returns true if the given project root contains a `.devflow/memory/` directory.
146
178
  * Treats unexpected errors (e.g. EACCES) as absent to avoid false positives.
@@ -195,16 +227,16 @@ export async function cleanQueueFiles(projectPaths) {
195
227
  }
196
228
  export const memoryCommand = new Command('memory')
197
229
  .description('Enable, disable, or clean up working memory (session context preservation)')
198
- .option('--enable', 'Enable working memory')
199
- .option('--disable', 'Disable working memory')
230
+ .option('--enable', 'Enable working memory in every project')
231
+ .option('--disable', 'Disable working memory in every project')
200
232
  .option('--status', 'Show current state')
201
233
  .option('--clear', 'Clean up queue files from projects')
202
234
  .action(async (options) => {
203
235
  const hasFlag = options.enable || options.disable || options.status || options.clear;
204
236
  if (!hasFlag) {
205
237
  p.intro(color.bgCyan(color.white(' Working Memory ')));
206
- p.note(`${color.cyan('devflow memory --enable')} Add memory hooks\n` +
207
- `${color.cyan('devflow memory --disable')} Remove memory hooks\n` +
238
+ p.note(`${color.cyan('devflow memory --enable')} Enable working memory (every project)\n` +
239
+ `${color.cyan('devflow memory --disable')} Disable working memory (every project)\n` +
208
240
  `${color.cyan('devflow memory --status')} Check current state\n` +
209
241
  `${color.cyan('devflow memory --clear')} Clean up queue files`, 'Usage');
210
242
  p.outro(color.dim('Memory hooks provide automatic session context preservation'));
@@ -260,90 +292,68 @@ export const memoryCommand = new Command('memory')
260
292
  : 'No queue files found to clean');
261
293
  return;
262
294
  }
263
- const claudeDir = getClaudeDirectory();
264
- const settingsPath = path.join(claudeDir, 'settings.json');
295
+ const settingsPath = path.join(getClaudeDirectory(), 'settings.json');
296
+ const devflowDir = getDevFlowDirectory();
265
297
  let settingsContent;
266
298
  try {
267
299
  settingsContent = await fs.readFile(settingsPath, 'utf-8');
268
300
  }
269
301
  catch {
270
- if (options.status) {
271
- p.log.info('Working memory: disabled (no settings.json found)');
272
- return;
273
- }
274
- // Create minimal settings.json
275
302
  settingsContent = '{}';
276
303
  }
277
- // Resolve current project root for feature config
278
- const gitRoot = await getGitRoot();
279
304
  if (options.status) {
280
- if (!gitRoot) {
281
- p.log.info(`Working memory: ${color.dim('disabled')} (not in a git project)`);
282
- return;
283
- }
305
+ // D-FEATURES-MACHINE-WIDE: one switch, the manifest's. The hook count is
306
+ // reported beside it because the hooks are how that switch takes effect.
307
+ const enabled = await readMachineFeature(devflowDir, 'memory');
284
308
  const count = countMemoryHooks(settingsContent);
285
309
  const total = Object.keys(MEMORY_HOOK_CONFIG).length;
286
- // Also check feature config: hooks may be registered but feature toggled off
287
- const featureEnabled = await isFeatureEnabled(gitRoot, 'memory');
288
- if (count === total && featureEnabled) {
310
+ if (enabled && count === total) {
289
311
  p.log.info(`Working memory: ${color.green('enabled')} (${total}/${total} hooks)`);
290
312
  }
291
- else if (count === 0 || !featureEnabled) {
313
+ else if (!enabled) {
292
314
  p.log.info(`Working memory: ${color.dim('disabled')}`);
293
315
  }
294
316
  else {
295
- p.log.info(`Working memory: ${color.yellow(`partial (${count}/${total} hooks)`)} — run --enable to fix`);
317
+ p.log.info(`Working memory: ${color.yellow(`enabled, but ${count}/${total} hooks registered`)} — ` +
318
+ `run ${color.cyan('devflow memory --enable')} to fix`);
296
319
  }
297
320
  return;
298
321
  }
299
- const devflowDir = getDevFlowDirectory();
300
- if (options.enable) {
301
- // D: --enable both installs hooks AND writes feature config, while --disable only
302
- // writes feature config. This asymmetry is intentional: capture hooks are shared
303
- // across features (memory, learning, decisions) and must never be removed by a
304
- // single-feature disable. --enable must still install them on first use.
305
- const alreadyHasHooks = hasMemoryHooks(settingsContent);
306
- const alreadyEnabled = alreadyHasHooks && (gitRoot ? await isFeatureEnabled(gitRoot, 'memory') : false);
307
- if (alreadyEnabled) {
308
- p.log.info('Working memory already enabled');
309
- }
310
- else if (alreadyHasHooks) {
311
- // Hooks are registered but config has memory:false — re-enable via config
312
- p.log.success('Working memory enabled — configuration updated');
313
- p.log.info(color.dim('Session context will be automatically preserved across conversations'));
314
- }
315
- else {
316
- const updated = addMemoryHooks(settingsContent, devflowDir);
317
- await writeFileAtomicExclusive(settingsPath, updated);
318
- p.log.success('Working memory enabled — hooks registered');
319
- p.log.info(color.dim('Session context will be automatically preserved across conversations'));
320
- }
321
- // Update config to enable memory feature
322
- if (gitRoot) {
323
- await updateFeature(gitRoot, 'memory', true);
324
- }
325
- await syncManifestFeature(getDevFlowDirectory(), 'memory', true);
322
+ // --enable / --disable: the machine-wide switch, converged exactly as
323
+ // `devflow init --memory / --no-memory` converges it (D-FEATURES-MACHINE-WIDE).
324
+ // The settings transform runs FIRST: it is the step that can reject its
325
+ // input (malformed JSON), and the switch must not be recorded unless the
326
+ // hooks that enact it can follow.
327
+ const enabled = options.enable === true;
328
+ let converged;
329
+ try {
330
+ converged = convergeMemoryHooks(settingsContent, enabled, devflowDir);
331
+ }
332
+ catch (err) {
333
+ p.log.error(`Could not update ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
334
+ process.exitCode = 1;
326
335
  return;
327
336
  }
328
- if (options.disable) {
329
- // Hooks remain registered (shared with other features).
330
- // Disable by writing memory: false to config only — hooks are not removed.
331
- if (gitRoot) {
332
- await updateFeature(gitRoot, 'memory', false);
333
- // Drain orphaned queue files so stale turns don't process on re-enable
334
- await Promise.all([
335
- fs.unlink(getPendingTurnsPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
336
- throw e; }),
337
- fs.unlink(getPendingTurnsProcessingPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
338
- throw e; }),
339
- ]);
340
- await syncManifestFeature(getDevFlowDirectory(), 'memory', false);
341
- p.log.success('Working memory disabled — configuration updated');
342
- }
343
- else {
344
- p.log.warn('Could not resolve git root — configuration not updated');
345
- }
337
+ const recorded = await writeMachineFeature(devflowDir, 'memory', enabled);
338
+ if (!recorded.ok) {
339
+ p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
340
+ process.exitCode = 1;
346
341
  return;
347
342
  }
343
+ if (converged !== settingsContent) {
344
+ await writeFileAtomicExclusive(settingsPath, converged);
345
+ }
346
+ if (enabled) {
347
+ p.log.success('Working memory enabled in every project');
348
+ p.log.info(color.dim('Session context will be automatically preserved across conversations'));
349
+ return;
350
+ }
351
+ // Drain the current project's queue, as init does. Outside a git project
352
+ // there is no project queue to drain, and the switch itself still applies.
353
+ const gitRoot = await getGitRoot();
354
+ if (gitRoot) {
355
+ await drainMemoryQueue(gitRoot);
356
+ }
357
+ p.log.success('Working memory disabled in every project');
348
358
  });
349
359
  //# sourceMappingURL=memory.js.map