devflow-kit 2.4.0 → 3.0.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 (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,200 @@
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 skillName - Bare name of the skill hosting the generated references,
28
+ * rendered `devflow:`-prefixed. Defaults to the core constant the build path and
29
+ * the installer's overlay trigger both read, so the renderer is never a third
30
+ * independent statement of which skill owns them — the divergence PF-013
31
+ * describes, where changing the answer means finding every retyped spelling and
32
+ * nothing fails if one is missed.
33
+ */
34
+ export function formatOverlaySummary(report, skillName = SKILL_REFS_SKILL_NAME) {
35
+ const lines = [];
36
+ if (report.overlaidRefs.length > 0) {
37
+ lines.push({
38
+ level: 'info',
39
+ message: `Installed ${report.overlaidRefs.length} generated skill reference(s) for ` +
40
+ prefixSkillName(skillName),
41
+ });
42
+ }
43
+ for (const failure of report.overlayFailures) {
44
+ lines.push({
45
+ level: 'warn',
46
+ message: `Could not refresh the generated references for ${overlayUnitLabel(failure.unit)} ` +
47
+ `(${failure.error}) — ${describeOverlayFailureState(failure.state)}`,
48
+ });
49
+ }
50
+ return lines;
51
+ }
52
+ /**
53
+ * The half of an overlay warning that describes what is actually on disk.
54
+ *
55
+ * One sentence per state, each true of that state and of no other. A single shared
56
+ * sentence — "the previously installed files were left unchanged" — is true of the first
57
+ * arm only, and would read loudest over the arms it fits worst: a set left
58
+ * half-refreshed, and a unit whose only surviving copy is a backup path the user has to
59
+ * be told about.
60
+ *
61
+ * Exported because `devflow tracker --set` renders the same states when it aborts
62
+ * on an overlay failure (applies PF-013 — one sentence per state, in one place,
63
+ * rather than a second wording that drifts).
64
+ *
65
+ * Exhaustive over {@link OverlayFailureState} — a new state added to the union without a
66
+ * sentence here is a compile error, not a state that silently prints nothing.
67
+ */
68
+ export function describeOverlayFailureState(state) {
69
+ switch (state.kind) {
70
+ case 'installed-unchanged':
71
+ return 'the previously installed files were left unchanged';
72
+ case 'not-installed':
73
+ return (`nothing is installed in their place, so ${state.absent.length} reference(s) the ` +
74
+ `agent is told to load are absent: ${state.absent.join(', ')}`);
75
+ case 'partially-refreshed':
76
+ return (`${state.refreshed.length} of ${state.refreshed.length + state.stale.length} ` +
77
+ `document(s) had already been replaced, so the set is part new and part old — ` +
78
+ `still on the previous install: ${state.stale.join(', ') || 'none'}`);
79
+ case 'restore-failed':
80
+ return (`the displaced copy could NOT be put back (${state.restoreError}), so nothing is ` +
81
+ `installed there now — the only surviving copy is "${state.recoveryPath}", which ` +
82
+ `this run's stale-reference prune was skipped to preserve`);
83
+ default: {
84
+ const _exhaustive = state;
85
+ void _exhaustive;
86
+ return 'the state it was left in is unknown';
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * The install summary's tracker rows.
92
+ *
93
+ * Two facts the install has no other visible trace of:
94
+ *
95
+ * - WHICH provider is the machine default. The sentinel is a dotfile and every
96
+ * provider's mechanics are installed (D-INSTALL-ALL-PROVIDERS), so nothing on
97
+ * disk says which one the machine selected. `(default)` distinguishes "github
98
+ * because I chose it" from "github because nothing was chosen"; `(was jira)`
99
+ * is what makes a self-heal or a `--reset` collapse legible rather than silent.
100
+ * - WHAT this run moved: references written or pruned, and the Tracker agent
101
+ * when this run wrote it.
102
+ *
103
+ * The delta line is emitted only when something moved: on a steady-state re-init
104
+ * the counts are noise.
105
+ *
106
+ * Pure function — returns lines, logs nothing (applies ADR-013).
107
+ *
108
+ * @param previous - The provider recorded by the PRIOR manifest, or undefined on
109
+ * a first install. Rendered only when it differs from `provider`.
110
+ * @param isDefault - Whether `provider` is the registry default.
111
+ */
112
+ export function formatTrackerAssetSummary(input) {
113
+ const lines = [];
114
+ const changed = input.previous !== undefined && input.previous !== input.provider;
115
+ const qualifier = changed
116
+ ? ` ${color.dim(`(was ${input.previous})`)}`
117
+ : input.isDefault ? ` ${color.dim('(default)')}` : '';
118
+ lines.push({ level: 'info', message: `Tracker: ${input.provider}${qualifier}` });
119
+ const agentNote = input.agent === 'unchanged' ? '' : `, tracker agent ${input.agent}`;
120
+ if (input.installedRefs > 0 || input.removedRefs > 0 || agentNote !== '') {
121
+ lines.push({
122
+ level: 'info',
123
+ message: `Tracker assets: +${input.installedRefs} reference(s), ` +
124
+ `−${input.removedRefs} reference(s)${agentNote}`,
125
+ });
126
+ }
127
+ return lines;
128
+ }
129
+ /**
130
+ * Did this run's plugin selection match the one already on disk?
131
+ *
132
+ * The question {@link formatSkillScopeSummary}'s `pluginListUnchanged` asks, and
133
+ * a separate function because the two halves fail differently and only one of
134
+ * them had executed evidence (design review L2): the renderer's behaviour given
135
+ * an answer, and the answer itself.
136
+ *
137
+ * Two clauses, and the first one is the one a set comparison alone would lose:
138
+ *
139
+ * - **A prior manifest must EXIST.** `null` is a first install. Nothing was
140
+ * removed from a user who had nothing, so there is no upgrade to explain,
141
+ * and comparing "no previous selection" against this run's would otherwise
142
+ * read as a match whenever both are empty.
143
+ * - **The plugin SETS must be equal**, not the arrays: order is an artifact of
144
+ * how the selection was assembled, and a duplicate name in either list is a
145
+ * manifest detail rather than a different selection.
146
+ *
147
+ * Pure function (applies ADR-013).
148
+ *
149
+ * @param previousPlugins - `manifest.plugins` as it stands before this run, or
150
+ * `null` when there is no prior manifest.
151
+ * @param effectivePluginNames - What this run installs.
152
+ */
153
+ export function isPluginListUnchanged(previousPlugins, effectivePluginNames) {
154
+ if (previousPlugins === null)
155
+ return false;
156
+ const previous = new Set(previousPlugins);
157
+ const effective = new Set(effectivePluginNames);
158
+ return previous.size === effective.size && [...effective].every(name => previous.has(name));
159
+ }
160
+ /**
161
+ * Turn the skill-scoping half of an InstallReport into summary lines.
162
+ *
163
+ * Two facts the filesystem cannot tell the user apart from an install that never
164
+ * happened:
165
+ *
166
+ * - a REMOVED skill. Scoping the install means a re-init after deselecting a
167
+ * plugin silently deletes skills the previous install carried. The line is
168
+ * emitted only when the plugin list is UNCHANGED, because that is the case
169
+ * the user did not ask for: they re-ran init expecting nothing to move, and
170
+ * the scoping change is what moved it. When they deselected a plugin
171
+ * themselves, the removal is the thing they asked for and needs no notice.
172
+ * - a DORMANT shadow. `~/.devflow/skills/{name}/` is never deleted, so an
173
+ * inactive shadow and an applied one look identical from disk.
174
+ *
175
+ * `pluginListUnchanged` is the caller's answer to "did the selection move?", and
176
+ * it is FALSE when there is no prior manifest: a first install removed nothing a
177
+ * user had, so there is no upgrade to explain (design review L2).
178
+ *
179
+ * Pure function — returns lines, logs nothing (applies ADR-013).
180
+ */
181
+ export function formatSkillScopeSummary(report, pluginListUnchanged) {
182
+ const lines = [];
183
+ if (pluginListUnchanged && report.removedSkills.length > 0) {
184
+ const names = [...report.removedSkills].sort().join(', ');
185
+ lines.push({
186
+ level: 'info',
187
+ message: `Removed ${report.removedSkills.length} skill(s) no selected plugin requires: ${names}. ` +
188
+ `Re-run devflow init and select the plugin that provides them to keep them.`,
189
+ });
190
+ }
191
+ for (const name of report.dormantShadows) {
192
+ lines.push({
193
+ level: 'info',
194
+ message: `Shadow for ${name} is inactive — the plugin that uses it is not selected ` +
195
+ `(${color.dim('kept in ~/.devflow/skills/')})`,
196
+ });
197
+ }
198
+ return lines;
199
+ }
200
+ //# 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 (a repository can opt out)')
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-NARROW-ONLY (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, plus the repository's narrowing when a repo layer narrows it.
5
9
  */
6
10
  import { promises as fs } from 'fs';
7
11
  import * as path from 'path';
@@ -9,8 +13,8 @@ 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';
17
+ import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../../core/evidence-policy.js';
14
18
  import { getFeaturesDir } from '../../../core/project-paths.js';
15
19
  async function getWorktreePath() {
16
20
  return (await getGitRoot()) ?? process.cwd();
@@ -39,46 +43,40 @@ async function countKnowledgeBases(worktreePath) {
39
43
  export async function handleToggle(options) {
40
44
  if (!options.enable && !options.disable && !options.status)
41
45
  return;
42
- const worktreePath = await getWorktreePath();
43
46
  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.');
47
+ if (options.status) {
48
+ p.intro(color.cyan('Feature Knowledge Status'));
49
+ const enabled = await readMachineFeature(devflowDir, 'knowledge');
50
+ const kbCount = await countKnowledgeBases(await getWorktreePath());
51
+ p.log.info(`Status: ${enabled ? color.green('enabled') : color.yellow('disabled')}`);
52
+ const settingsModule = loadSettingsModule();
53
+ const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'knowledge') : null;
54
+ if (narrowed !== null)
55
+ p.log.info(`Effective here: ${color.yellow(narrowed)}`);
56
+ const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
57
+ if (trackedWarning !== null)
58
+ p.log.warn(trackedWarning);
59
+ p.log.info(`Knowledge bases: ${kbCount}`);
57
60
  p.outro('');
61
+ return;
58
62
  }
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.');
63
+ const enabled = options.enable === true;
64
+ p.intro(color.cyan(`${enabled ? 'Enable' : 'Disable'} Feature Knowledge Bases`));
65
+ const recorded = await writeMachineFeature(devflowDir, 'knowledge', enabled);
66
+ if (!recorded.ok) {
67
+ p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
68
+ process.exitCode = 1;
72
69
  p.outro('');
70
+ return;
71
+ }
72
+ if (enabled) {
73
+ p.log.success('Feature knowledge bases enabled in every project (a repository can opt out)');
74
+ p.log.info('Knowledge bases are created automatically when workflows detect documented area changes.');
73
75
  }
74
76
  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('');
77
+ p.log.success('Feature knowledge bases disabled in every project');
78
+ p.log.info('Existing knowledge bases preserved. Write-back skipped while disabled.');
82
79
  }
80
+ p.outro('');
83
81
  }
84
82
  //# sourceMappingURL=toggle.js.map
@@ -4,10 +4,10 @@ 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';
8
+ import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
9
9
  import { getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
10
- import { getGitRoot } from '../../core/git.js';
10
+ import { getLedgerRoot } from '../../core/ledger-root.js';
11
11
  import { sweepLegacyDreamMarkers, drainLearningQueue } from '../../core/learning-queue-cleanup.js';
12
12
  import { readObservations, warnIfInvalid, } from '../../core/observation-io.js';
13
13
  // ---------------------------------------------------------------------------
@@ -15,8 +15,8 @@ import { readObservations, warnIfInvalid, } from '../../core/observation-io.js';
15
15
  // ---------------------------------------------------------------------------
16
16
  function printUsage() {
17
17
  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` +
18
+ p.note(`${color.cyan('devflow learning --enable')} Enable learning in every project (a repository can opt out)\n` +
19
+ `${color.cyan('devflow learning --disable')} Disable learning in every project (drains this project's queue)\n` +
20
20
  `${color.cyan('devflow learning --status')} Show learning status\n` +
21
21
  `${color.cyan('devflow learning --list')} Show all observations\n` +
22
22
  `${color.cyan('devflow learning --configure')} Configuration wizard\n` +
@@ -25,25 +25,39 @@ function printUsage() {
25
25
  p.outro(color.dim('Detects architectural decisions and known pitfalls from your sessions'));
26
26
  }
27
27
  /**
28
- * Resolve the git root for a state-mutating subcommand, warning and
28
+ * Resolve the ledger root for a state-mutating subcommand, warning and
29
29
  * returning null if the caller isn't inside a git project. `actionSuffix`
30
30
  * completes "Could not resolve git root — {actionSuffix}".
31
+ *
32
+ * D-LEDGER-MAIN-WORKTREE: every subcommand here resolves the ledger with
33
+ * getLedgerRoot — the hooks' DF_LEDGER_ROOT rule — so in a linked worktree it
34
+ * reads, clears and drains the main checkout's ledger the hooks write.
31
35
  */
32
- async function requireGitRoot(actionSuffix) {
33
- const gitRoot = await getGitRoot();
34
- if (!gitRoot) {
36
+ async function requireLedgerRoot(actionSuffix) {
37
+ const ledgerRoot = await getLedgerRoot();
38
+ if (!ledgerRoot) {
35
39
  p.log.warn(`Could not resolve git root — ${actionSuffix}`);
36
40
  }
37
- return gitRoot;
41
+ return ledgerRoot;
38
42
  }
39
43
  async function handleStatus() {
40
- const gitRoot = await getGitRoot();
41
- if (!gitRoot) {
42
- p.log.info('Learning: disabled (not in a git project)');
44
+ // D-FEATURES-NARROW-ONLY: the machine switch is the manifest's and reads the
45
+ // same from every directory; a repository layer can only narrow it, and adds a
46
+ // line only when it does. The observation counts are per-project.
47
+ const enabled = await readMachineFeature(getDevFlowDirectory(), 'learning');
48
+ const settingsModule = loadSettingsModule();
49
+ const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'learning') : null;
50
+ const stateLine = `Learning: ${enabled ? 'enabled' : 'disabled'}`
51
+ + (narrowed === null ? '' : `\nEffective here: ${narrowed}`);
52
+ const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
53
+ if (trackedWarning !== null)
54
+ p.log.warn(trackedWarning);
55
+ const ledgerRoot = await getLedgerRoot();
56
+ if (!ledgerRoot) {
57
+ p.log.info(`${stateLine}\nObservations: not in a git project`);
43
58
  return;
44
59
  }
45
- const logPath = getDecisionsLogPath(gitRoot);
46
- const enabled = await isFeatureEnabled(gitRoot, 'learning');
60
+ const logPath = getDecisionsLogPath(ledgerRoot);
47
61
  const { observations, invalidCount } = await readObservations(logPath);
48
62
  const decisionObs = observations.filter(o => o.type === 'decision' || o.type === 'pitfall');
49
63
  const decisions = observations.filter(o => o.type === 'decision');
@@ -52,7 +66,7 @@ async function handleStatus() {
52
66
  const ready = decisionObs.filter(o => o.status === 'ready');
53
67
  const observing = decisionObs.filter(o => o.status === 'observing');
54
68
  const deprecated = decisionObs.filter(o => o.status === 'deprecated');
55
- const lines = [`Learning: ${enabled ? 'enabled' : 'disabled'}`];
69
+ const lines = [stateLine];
56
70
  if (decisionObs.length === 0) {
57
71
  lines.push('Observations: none');
58
72
  }
@@ -65,12 +79,12 @@ async function handleStatus() {
65
79
  warnIfInvalid(invalidCount);
66
80
  }
67
81
  async function handleList() {
68
- // Resolve the log from the git root (matches --status, --clear, --reset,
69
- // --disable) so `--list` run from a subdirectory finds the real log
70
- // instead of a nonexistent one under process.cwd(). Falls back to cwd
71
- // when not in a git project, preserving the prior behavior for that case.
72
- const gitRoot = await getGitRoot();
73
- const logPath = getDecisionsLogPath(gitRoot ?? process.cwd());
82
+ // Resolve the log from the ledger root (matches --status, --clear, --reset,
83
+ // --disable) so `--list` run from a subdirectory or a linked worktree finds
84
+ // the real log instead of a nonexistent one under process.cwd(). Falls back
85
+ // to cwd when not in a git project, preserving the prior behavior for that case.
86
+ const ledgerRoot = await getLedgerRoot();
87
+ const logPath = getDecisionsLogPath(ledgerRoot ?? process.cwd());
74
88
  let logExists = true;
75
89
  try {
76
90
  await fs.access(logPath);
@@ -148,19 +162,22 @@ async function handleConfigure() {
148
162
  p.log.success(`Global config written to ${color.dim(path.join(globalDir, 'learning.json'))}`);
149
163
  }
150
164
  else {
151
- const learningDir = getLearningDir(process.cwd());
152
- await fs.mkdir(learningDir, { recursive: true });
153
- const projectConfigPath = getLearningTuningConfigPath(process.cwd());
165
+ // D-LEDGER-MAIN-WORKTREE: session-start-context reads the project tuning config
166
+ // from the ledger ($LEDGER_ROOT/.devflow/learning/), so write it there — the
167
+ // main checkout in a linked worktree; the current directory outside git.
168
+ const projectRoot = (await getLedgerRoot()) ?? process.cwd();
169
+ await fs.mkdir(getLearningDir(projectRoot), { recursive: true });
170
+ const projectConfigPath = getLearningTuningConfigPath(projectRoot);
154
171
  await fs.writeFile(projectConfigPath, configJson, 'utf-8');
155
172
  p.log.success(`Project config written to ${color.dim(projectConfigPath)}`);
156
173
  }
157
174
  p.outro(color.green('Configuration saved.'));
158
175
  }
159
176
  async function handleReset() {
160
- const gitRoot = await requireGitRoot('reset not performed');
161
- if (!gitRoot)
177
+ const ledgerRoot = await requireLedgerRoot('reset not performed');
178
+ if (!ledgerRoot)
162
179
  return;
163
- const lockDir = getDecisionsLockDir(gitRoot);
180
+ const lockDir = getDecisionsLockDir(ledgerRoot);
164
181
  // Ensure the parent directory exists so a second reset (after .devflow/learning/
165
182
  // was already removed) does not fail with ENOENT and emit a false contention error.
166
183
  await fs.mkdir(path.dirname(lockDir), { recursive: true });
@@ -187,13 +204,13 @@ async function handleReset() {
187
204
  // Remove the entire learning directory (contains queue files, content files,
188
205
  // ledger, and tuning config). Single-dir semantics: all learning state lives here.
189
206
  try {
190
- await fs.rm(getLearningDir(gitRoot), { recursive: true, force: true });
207
+ await fs.rm(getLearningDir(ledgerRoot), { recursive: true, force: true });
191
208
  }
192
209
  catch { /* best effort */ }
193
210
  // Clean legacy dream marker-pipeline stamps from old installs.
194
211
  // Best-effort: sweeps the now-absent dir silently (ENOENT-tolerant).
195
212
  try {
196
- await sweepLegacyDreamMarkers(getLearningDir(gitRoot));
213
+ await sweepLegacyDreamMarkers(getLearningDir(ledgerRoot));
197
214
  }
198
215
  catch { /* best effort */ }
199
216
  p.log.success('Reset complete — removed .devflow/learning/ state.');
@@ -206,10 +223,10 @@ async function handleReset() {
206
223
  }
207
224
  }
208
225
  async function handleClear() {
209
- const gitRoot = await requireGitRoot('clear not performed');
210
- if (!gitRoot)
226
+ const ledgerRoot = await requireLedgerRoot('clear not performed');
227
+ if (!ledgerRoot)
211
228
  return;
212
- const decisionsLogPath = getDecisionsLogPath(gitRoot);
229
+ const decisionsLogPath = getDecisionsLogPath(ledgerRoot);
213
230
  try {
214
231
  await fs.access(decisionsLogPath);
215
232
  }
@@ -232,35 +249,40 @@ async function handleClear() {
232
249
  // on the next session — mirrors memory.ts's drain-on-disable behavior for
233
250
  // the sibling memory queue. A mid-run Learning agent whose claimed batch
234
251
  // vanishes aborts without changes — the desired outcome of clearing.
235
- await drainLearningQueue(gitRoot);
252
+ await drainLearningQueue(ledgerRoot);
236
253
  p.log.success('Decisions log cleared.');
237
254
  }
238
- async function handleEnable() {
239
- const gitRoot = await requireGitRoot('configuration not updated');
240
- if (!gitRoot)
255
+ /**
256
+ * `--enable` / `--disable`: the machine-wide switch (D-FEATURES-NARROW-ONLY),
257
+ * converged exactly as `devflow init --learning / --no-learning` converges it —
258
+ * the manifest value, and on disable a drained queue in the current project.
259
+ * Never requires a git root: the switch is not a per-project setting.
260
+ */
261
+ async function handleToggle(enabled) {
262
+ const recorded = await writeMachineFeature(getDevFlowDirectory(), 'learning', enabled);
263
+ if (!recorded.ok) {
264
+ p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
265
+ process.exitCode = 1;
241
266
  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)
267
+ }
268
+ if (enabled) {
269
+ p.log.success('Learning enabled in every project (a repository can opt out)');
270
+ p.log.info(color.dim('Architectural decisions and pitfalls will be detected from your sessions'));
250
271
  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
272
+ }
273
+ // Drain the current project's learning (decisions-detection) queue so stale
274
+ // turns don't process on re-enable. A mid-run Learning agent whose claimed
255
275
  // 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');
276
+ const ledgerRoot = await getLedgerRoot();
277
+ if (ledgerRoot) {
278
+ await drainLearningQueue(ledgerRoot);
279
+ }
280
+ p.log.success('Learning disabled in every project');
259
281
  }
260
282
  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')
283
+ .description('Enable or disable learning (decision/pitfall detection) in every project')
284
+ .option('--enable', 'Enable learning in every project (a repository can opt out)')
285
+ .option('--disable', 'Disable learning in every project')
264
286
  .option('--status', 'Show learning status and observation counts')
265
287
  .option('--list', 'Show all decision/pitfall observations sorted by confidence')
266
288
  .option('--configure', 'Interactive configuration wizard for learning.json')
@@ -300,11 +322,11 @@ export const learningCommand = new Command('learning')
300
322
  return;
301
323
  }
302
324
  if (options.enable) {
303
- await handleEnable();
325
+ await handleToggle(true);
304
326
  return;
305
327
  }
306
328
  if (options.disable) {
307
- await handleDisable();
329
+ await handleToggle(false);
308
330
  return;
309
331
  }
310
332
  });
@@ -1,3 +1,4 @@
1
+ import { devflowHookOwner, hasHook, removeHooks, } from '../../targets/claude-code/hooks.js';
1
2
  // ─── Dream worker hook cleanup ──────────────────────────────────────────────
2
3
  //
3
4
  // The spawn-dream-worker SessionStart hook belonged to the retired detached
@@ -6,27 +7,23 @@
6
7
  // own. remove/has exist for upgrade cleanup: init and uninstall strip any
7
8
  // stale entry left in settings.json by a prior install.
8
9
  const SPAWN_DREAM_WORKER_MARKER = 'spawn-dream-worker';
10
+ /**
11
+ * D-EXACT-HOOK-OWNER: the dream-worker hook is devflow's only when its command
12
+ * ends in `/scripts/hooks/run-hook spawn-dream-worker` (hooks.ts), under any
13
+ * directory — the one form it was ever registered in.
14
+ */
15
+ const isDreamHook = devflowHookOwner([SPAWN_DREAM_WORKER_MARKER]);
9
16
  /**
10
17
  * Remove the spawn-dream-worker hook from settings JSON.
11
18
  * Idempotent — returns unchanged JSON if hook not present.
12
- * Preserves all other SessionStart hooks (session-start-memory, session-start-context).
19
+ * Removes the single hook, so the other hooks of its matcher group and every
20
+ * other SessionStart group (session-start-memory, session-start-context) stay in place.
13
21
  */
14
22
  export function removeDreamHook(settingsJson) {
15
23
  const settings = JSON.parse(settingsJson);
16
- if (!settings.hooks?.SessionStart) {
17
- return settingsJson;
18
- }
19
- const before = settings.hooks.SessionStart.length;
20
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter((matcher) => !matcher.hooks.some((h) => h.command.includes(SPAWN_DREAM_WORKER_MARKER)));
21
- if (settings.hooks.SessionStart.length === before) {
24
+ if (!removeHooks(settings, 'SessionStart', isDreamHook)) {
22
25
  return settingsJson;
23
26
  }
24
- if (settings.hooks.SessionStart.length === 0) {
25
- delete settings.hooks.SessionStart;
26
- }
27
- if (Object.keys(settings.hooks).length === 0) {
28
- delete settings.hooks;
29
- }
30
27
  return JSON.stringify(settings, null, 2) + '\n';
31
28
  }
32
29
  /**
@@ -35,6 +32,6 @@ export function removeDreamHook(settingsJson) {
35
32
  */
36
33
  export function hasDreamHook(input) {
37
34
  const settings = typeof input === 'string' ? JSON.parse(input) : input;
38
- return settings.hooks?.SessionStart?.some((matcher) => matcher.hooks.some((h) => h.command.includes(SPAWN_DREAM_WORKER_MARKER))) ?? false;
35
+ return hasHook(settings, 'SessionStart', isDreamHook);
39
36
  }
40
37
  //# sourceMappingURL=legacy-hooks.js.map