devflow-kit 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +479 -1
  2. package/README.md +15 -15
  3. package/dist/agents/code.md +6 -8
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +7 -8
  6. package/dist/agents/git.md +3 -3
  7. package/dist/agents/harden.md +1 -1
  8. package/dist/agents/refactor.md +2 -4
  9. package/dist/agents/review.md +4 -4
  10. package/dist/agents/skim.md +2 -2
  11. package/dist/agents/triage.md +9 -2
  12. package/dist/cli/commands/capture.js +1 -1
  13. package/dist/cli/commands/curation-move-io.js +102 -0
  14. package/dist/cli/commands/curation-move-step.js +374 -0
  15. package/dist/cli/commands/curation.js +808 -0
  16. package/dist/cli/commands/gate-level-prompts.js +428 -0
  17. package/dist/cli/commands/gates.js +35 -11
  18. package/dist/cli/commands/init-seed.js +11 -8
  19. package/dist/cli/commands/init.js +244 -57
  20. package/dist/cli/commands/memory.js +60 -13
  21. package/dist/cli/commands/prompt-io.js +37 -0
  22. package/dist/cli/commands/uninstall.js +157 -17
  23. package/dist/cli.js +2 -2
  24. package/dist/commands/bug-analysis.md +25 -36
  25. package/dist/commands/code-review.md +40 -44
  26. package/dist/commands/debug.md +5 -5
  27. package/dist/commands/dynamic-build.md +26 -25
  28. package/dist/commands/dynamic-plan.md +5 -5
  29. package/dist/commands/dynamic-profile.md +5 -5
  30. package/dist/commands/dynamic-tickets.md +5 -5
  31. package/dist/commands/explore.md +6 -6
  32. package/dist/commands/implement.md +53 -82
  33. package/dist/commands/plan.md +6 -6
  34. package/dist/commands/release.md +5 -5
  35. package/dist/commands/research.md +6 -6
  36. package/dist/commands/resolve.md +39 -41
  37. package/dist/commands/self-review.md +5 -13
  38. package/dist/core/area-pause-notice.js +44 -0
  39. package/dist/core/assets.js +20 -20
  40. package/dist/core/curate-tuning-config.js +5 -6
  41. package/dist/core/curation-move-notice.js +84 -0
  42. package/dist/core/curation-move-skip.js +35 -0
  43. package/dist/core/curation-move.js +35 -0
  44. package/dist/core/curation-queue-cleanup.js +135 -0
  45. package/dist/core/curation-store.js +99 -0
  46. package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
  47. package/dist/core/echo-value.js +11 -5
  48. package/dist/core/evidence-policy.js +11 -9
  49. package/dist/core/feature-switch.js +95 -19
  50. package/dist/core/fs-atomic.js +1 -1
  51. package/dist/core/gate-level.js +10 -2
  52. package/dist/core/git.js +16 -14
  53. package/dist/core/ledger-root.js +82 -22
  54. package/dist/core/linked-path.js +4 -4
  55. package/dist/core/manifest-lock.js +238 -0
  56. package/dist/core/manifest.js +58 -14
  57. package/dist/core/migrations.js +1 -13
  58. package/dist/core/observations.js +2 -2
  59. package/dist/core/plugins.js +19 -19
  60. package/dist/core/pool.js +26 -0
  61. package/dist/core/project-paths.js +103 -34
  62. package/dist/core/queue-drain.js +2 -2
  63. package/dist/core/tracked-areas.js +159 -0
  64. package/dist/core/tracker.js +1 -1
  65. package/dist/{learning-off → curation-off}/agents/code.md +6 -8
  66. package/dist/{learning-off → curation-off}/agents/fix.md +7 -8
  67. package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
  68. package/dist/{learning-off → curation-off}/agents/review.md +3 -3
  69. package/dist/{learning-off → curation-off}/agents/triage.md +8 -1
  70. package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
  71. package/dist/{learning-off → curation-off}/commands/code-review.md +37 -41
  72. package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
  73. package/dist/{learning-off → curation-off}/commands/dynamic-build.md +23 -22
  74. package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
  75. package/dist/{learning-off → curation-off}/commands/implement.md +50 -79
  76. package/dist/{learning-off → curation-off}/commands/plan.md +2 -2
  77. package/dist/{learning-off → curation-off}/commands/resolve.md +36 -38
  78. package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
  79. package/dist/hud/components/{learning-counts.js → curation-counts.js} +63 -17
  80. package/dist/hud/config.js +1 -1
  81. package/dist/hud/index.js +6 -6
  82. package/dist/hud/render.js +3 -3
  83. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  84. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  85. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  86. package/dist/skills/git/references/pr/validate-branch.md +8 -5
  87. package/dist/skills/git/references/publication-gate.md +6 -7
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
  89. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  90. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
  91. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  92. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  94. package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
  95. package/dist/targets/claude-code/installer.js +15 -15
  96. package/dist/targets/claude-code/language-stamp.js +5 -5
  97. package/dist/targets/claude-code/post-install.js +2 -2
  98. package/dist/targets/claude-code/tracker-servers.js +1 -1
  99. package/package.json +3 -2
  100. package/src/assets/agents/_partials/_code.mds +11 -11
  101. package/src/assets/agents/code.mds +6 -6
  102. package/src/assets/agents/curate.md +8 -8
  103. package/src/assets/agents/debug.mds +6 -6
  104. package/src/assets/agents/design.mds +18 -18
  105. package/src/assets/agents/evaluate.md +6 -12
  106. package/src/assets/agents/fix.mds +7 -6
  107. package/src/assets/agents/git.mds +3 -3
  108. package/src/assets/agents/harden.mds +7 -7
  109. package/src/assets/agents/knowledge.mds +23 -23
  110. package/src/assets/agents/refactor.mds +2 -2
  111. package/src/assets/agents/research.mds +15 -15
  112. package/src/assets/agents/review.mds +43 -43
  113. package/src/assets/agents/skim.mds +11 -11
  114. package/src/assets/agents/triage.mds +29 -22
  115. package/src/assets/agents/validate.md +30 -50
  116. package/src/assets/commands/_partials/_decisions.mds +22 -20
  117. package/src/assets/commands/_partials/_engine.mds +8 -8
  118. package/src/assets/commands/_partials/_knowledge.mds +29 -15
  119. package/src/assets/commands/_partials/_preamble.mds +4 -4
  120. package/src/assets/commands/_partials/_settings.mds +3 -3
  121. package/src/assets/commands/_partials/_wave.mds +7 -7
  122. package/src/assets/commands/bug-analysis.mds +37 -48
  123. package/src/assets/commands/code-review.mds +53 -57
  124. package/src/assets/commands/debug.mds +14 -14
  125. package/src/assets/commands/dynamic-build.mds +38 -37
  126. package/src/assets/commands/dynamic-plan.mds +22 -22
  127. package/src/assets/commands/dynamic-profile.mds +2 -2
  128. package/src/assets/commands/dynamic-tickets.mds +8 -8
  129. package/src/assets/commands/explore.mds +20 -20
  130. package/src/assets/commands/implement.mds +77 -98
  131. package/src/assets/commands/plan.mds +27 -27
  132. package/src/assets/commands/release.mds +12 -12
  133. package/src/assets/commands/research.mds +19 -19
  134. package/src/assets/commands/resolve.mds +66 -60
  135. package/src/assets/commands/self-review.mds +20 -20
  136. package/src/assets/mds/git/_ops.mds +9 -2
  137. package/src/assets/mds/git/_ops_pr.mds +21 -11
  138. package/src/assets/mds/git/_ops_steps.mds +20 -4
  139. package/src/assets/mds/git/_pr.mds +21 -18
  140. package/src/assets/mds/git/_references.mds +6 -7
  141. package/src/assets/mds/tracker/_github.mds +1 -4
  142. package/src/assets/mds/tracker/_jira.mds +1 -4
  143. package/src/assets/mds/tracker/_linear.mds +1 -4
  144. package/src/assets/rules/quality.md +1 -0
  145. package/src/assets/scripts/ci-wait.cjs +389 -92
  146. package/src/assets/scripts/hooks/background-memory-update +81 -3
  147. package/src/assets/scripts/hooks/capture-prompt +12 -12
  148. package/src/assets/scripts/hooks/capture-question +11 -11
  149. package/src/assets/scripts/hooks/capture-turn +12 -12
  150. package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
  151. package/src/assets/scripts/hooks/curation-move +293 -0
  152. package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
  153. package/src/assets/scripts/hooks/ensure-proxy +4 -4
  154. package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
  155. package/src/assets/scripts/hooks/git-marker +81 -0
  156. package/src/assets/scripts/hooks/json-helper.cjs +176 -51
  157. package/src/assets/scripts/hooks/json-parse +1 -1
  158. package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
  159. package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +308 -122
  160. package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
  161. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
  162. package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
  163. package/src/assets/scripts/hooks/lib/render-decisions.cjs +42 -22
  164. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +452 -0
  165. package/src/assets/scripts/hooks/memory-permission +10 -2
  166. package/src/assets/scripts/hooks/memory-worker +12 -1
  167. package/src/assets/scripts/hooks/pre-compact-memory +17 -16
  168. package/src/assets/scripts/hooks/queue-append +444 -102
  169. package/src/assets/scripts/hooks/resolve-project-root +2 -2
  170. package/src/assets/scripts/hooks/session-start-context +198 -72
  171. package/src/assets/scripts/hooks/session-start-memory +42 -43
  172. package/src/assets/scripts/lib/project-config.cjs +12 -5
  173. package/src/assets/scripts/pr-evidence.cjs +5 -2
  174. package/src/assets/scripts/resolve-base.cjs +1043 -0
  175. package/src/assets/scripts/resolve-settings.cjs +283 -45
  176. package/src/assets/scripts/verify-evidence.cjs +307 -75
  177. package/src/assets/skills/agent-references/SKILL.md +11 -2
  178. package/src/assets/skills/agent-references/references/commands/implement-levels.md +29 -0
  179. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
  180. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +25 -0
  181. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  182. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  183. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  184. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  185. package/src/assets/skills/docs-framework/SKILL.md +7 -7
  186. package/src/assets/skills/docs-framework/references/patterns.md +3 -4
  187. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  188. package/src/assets/skills/worktree-support/references/roots.md +2 -2
  189. package/templates/lint-pack/.lint-pack.conf +86 -0
  190. package/templates/lint-pack/README.md +190 -0
  191. package/templates/lint-pack/lint-pack.sh +905 -0
  192. package/dist/cli/commands/learning.js +0 -554
  193. package/dist/core/learning-queue-cleanup.js +0 -27
  194. package/dist/core/learning-store.js +0 -61
  195. /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
  196. /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
  197. /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
  198. /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
  199. /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
  200. /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
  201. /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
  202. /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
  203. /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
  204. /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
  205. /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
@@ -1,554 +0,0 @@
1
- import { Command } from 'commander';
2
- import { promises as fs } from 'fs';
3
- import * as path from 'path';
4
- import * as p from '@clack/prompts';
5
- import color from 'picocolors';
6
- import { getLearningDir, getCurateTuningConfigPath, } from '../../core/project-paths.js';
7
- import { loadShippedAgentDefaults } from '../../core/agent-models.js';
8
- import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
9
- import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
10
- import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
11
- import { applyLearningToggle, describeLearningConverge, readRunningVersion, } from '../../targets/claude-code/learning-install.js';
12
- import { getLedgerRoot } from '../../core/ledger-root.js';
13
- import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
14
- import { firstSymbolicLink } from '../../core/linked-path.js';
15
- import { formatRefusedDrain } from '../../core/queue-drain.js';
16
- import { formatLearningStoreUnavailable, loadLearningStore, } from '../../core/learning-store.js';
17
- // ---------------------------------------------------------------------------
18
- // Shared helpers
19
- // ---------------------------------------------------------------------------
20
- /** What the read and restore commands say about a project with no `.devflow/learning/`. */
21
- const NO_LEARNING_DATA = 'No learning data in this project yet.';
22
- /**
23
- * How long a writer here (--restore, --clear, --reset) waits for the learning lock
24
- * before it refuses as busy. An op holds the lock for milliseconds, so a longer
25
- * wait means a stuck holder, and the store's own 30 s wait would leave the command
26
- * looking hung.
27
- */
28
- const LOCK_WAIT_MS = 5000;
29
- /**
30
- * Code point ranges JSON.stringify leaves raw that a terminal may act on, or may
31
- * display reordered: DEL and the C1 controls, the directional marks, the line and
32
- * paragraph separators, and the bidirectional embeddings, overrides and isolates.
33
- */
34
- const TERMINAL_UNSAFE_RANGES = [
35
- [0x7f, 0x9f],
36
- [0x200e, 0x200f],
37
- [0x2028, 0x2029],
38
- [0x202a, 0x202e],
39
- [0x2066, 0x2069],
40
- ];
41
- /**
42
- * `value` as pretty JSON a terminal shows as written: every character in
43
- * TERMINAL_UNSAFE_RANGES becomes its `\uXXXX` escape. Such characters can occur
44
- * only inside JSON strings, where the escape means the same character, so the
45
- * text still parses back to `value`.
46
- */
47
- function terminalSafeJson(value) {
48
- let text = '';
49
- for (const ch of JSON.stringify(value, null, 2)) {
50
- const code = ch.codePointAt(0) ?? 0;
51
- const unsafe = TERMINAL_UNSAFE_RANGES.some(([low, high]) => code >= low && code <= high);
52
- text += unsafe ? `\\u${code.toString(16).padStart(4, '0')}` : ch;
53
- }
54
- return text;
55
- }
56
- /** `1 decision`, `2 decisions`. */
57
- function counted(count, noun) {
58
- return `${count} ${noun}${count === 1 ? '' : 's'}`;
59
- }
60
- /**
61
- * The learning store (D-LEARNING-STORE-SEAM), or null after reporting why it
62
- * cannot be used and setting exit code 1.
63
- */
64
- function requireStore() {
65
- const loaded = loadLearningStore();
66
- if (loaded.ok)
67
- return loaded.value;
68
- p.log.error(`Learning: ${formatLearningStoreUnavailable(loaded.error)}`);
69
- process.exitCode = 1;
70
- return null;
71
- }
72
- /** What a writer says when the store refused; `undone` completes "Nothing was …". */
73
- function storeRefusal(error, undone) {
74
- switch (error.kind) {
75
- case 'no-learning-dir': return NO_LEARNING_DATA;
76
- case 'busy': return `The learning store is busy: another run holds its lock. Nothing was ${undone}; try again in a moment.`;
77
- default: return error.message;
78
- }
79
- }
80
- /**
81
- * Ask `message` on a terminal and say whether to go on, logging `cancelled` when the
82
- * answer is no. With no terminal there is no one to ask: a script that passed the
83
- * flag has decided.
84
- */
85
- async function confirmOnTerminal(message, cancelled) {
86
- if (!process.stdin.isTTY)
87
- return true;
88
- const confirmed = await p.confirm({ message, initialValue: false });
89
- if (p.isCancel(confirmed) || !confirmed) {
90
- p.log.info(cancelled);
91
- return false;
92
- }
93
- return true;
94
- }
95
- /** True when `dir` is a directory; false when nothing, or something else, is there. */
96
- async function isDirectory(dir) {
97
- try {
98
- return (await fs.stat(dir)).isDirectory();
99
- }
100
- catch (err) {
101
- const code = err.code;
102
- if (code === 'ENOENT' || code === 'ENOTDIR')
103
- return false;
104
- throw err;
105
- }
106
- }
107
- // ---------------------------------------------------------------------------
108
- // Sub-command handlers
109
- // ---------------------------------------------------------------------------
110
- function printUsage() {
111
- p.intro(color.bgCyan(color.black(' Learning ')));
112
- p.note(`${color.cyan('devflow learning --enable')} Enable learning in every project (a repository can opt out)\n` +
113
- `${color.cyan('devflow learning --disable')} Disable learning in every project (drains this project's queue)\n` +
114
- `${color.cyan('devflow learning --status')} Show learning status and entry counts\n` +
115
- `${color.cyan('devflow learning --list')} List entries, inactive entries and observations\n` +
116
- `${color.cyan('devflow learning --show <id>')} Print one entry or observation as JSON\n` +
117
- `${color.cyan('devflow learning --restore <id>')} Make an inactive entry active again\n` +
118
- `${color.cyan('devflow learning --configure')} Configuration wizard\n` +
119
- `${color.cyan('devflow learning --clear')} Drop the observations no entry uses, and drain the queue\n` +
120
- `${color.cyan('devflow learning --reset')} Remove all learning state files`, 'Usage');
121
- p.outro(color.dim('Detects architectural decisions and known pitfalls from your sessions'));
122
- }
123
- /**
124
- * Resolve the ledger root for a state-mutating subcommand, warning and
125
- * returning null if the caller isn't inside a git project. `actionSuffix`
126
- * completes "Could not resolve git root — {actionSuffix}".
127
- *
128
- * D-LEDGER-MAIN-WORKTREE: every subcommand here resolves the ledger with
129
- * getLedgerRoot — the hooks' DF_LEDGER_ROOT rule — so in a linked worktree it
130
- * reads, clears and drains the main checkout's ledger the hooks write.
131
- */
132
- async function requireLedgerRoot(actionSuffix) {
133
- const ledgerRoot = await getLedgerRoot();
134
- if (!ledgerRoot) {
135
- p.log.warn(`Could not resolve git root — ${actionSuffix}`);
136
- }
137
- return ledgerRoot;
138
- }
139
- /**
140
- * The --status entry lines: active entries by type, inactive ones by status (in
141
- * the store's order, each status present), the active entries still in the v1
142
- * format — the migration still to do — and the observations.
143
- */
144
- function entryCountLines(listing, logRowCount, inactiveStatuses) {
145
- const active = listing.active;
146
- const decisions = active.filter(entry => entry.type === 'decision').length;
147
- const pitfalls = active.filter(entry => entry.type === 'pitfall').length;
148
- const byStatus = inactiveStatuses
149
- .map(status => ({ status, count: listing.inactive.filter(entry => entry.status === status).length }))
150
- .filter(({ count }) => count > 0)
151
- .map(({ status, count }) => `${status} ${count}`);
152
- const legacy = active.filter(entry => entry.schema === 1).length;
153
- return [
154
- `Entries: ${active.length} active (${counted(decisions, 'decision')}, ${counted(pitfalls, 'pitfall')}), `
155
- + `${listing.inactive.length} inactive${byStatus.length > 0 ? ` (${byStatus.join(', ')})` : ''}`,
156
- `Legacy v1 entries: ${legacy} of ${active.length} active`,
157
- `Observations: ${logRowCount} in the log, ${listing.observations.length} not yet promoted`,
158
- ];
159
- }
160
- /**
161
- * `--status`: the machine switch, then the counts the store reads. Reads only:
162
- * malformed lines are counted, never quarantined, and no scope is checked.
163
- */
164
- async function handleStatus() {
165
- // D-FEATURES-NARROW-ONLY: the machine switch is the manifest's and reads the
166
- // same from every directory; a repository layer can only narrow it, and adds a
167
- // line only when it does. The entry counts are per-project.
168
- const enabled = await readMachineFeature(getDevFlowDirectory(), 'learning');
169
- const settingsModule = loadSettingsModule();
170
- const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'learning') : null;
171
- const stateLine = `Learning: ${enabled ? 'enabled' : 'disabled'}`
172
- + (narrowed === null ? '' : `\nEffective here: ${narrowed}`);
173
- const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
174
- if (trackedWarning !== null)
175
- p.log.warn(trackedWarning);
176
- const ledgerRoot = await getLedgerRoot();
177
- if (!ledgerRoot) {
178
- p.log.info(`${stateLine}\nEntries: not in a git project`);
179
- return;
180
- }
181
- const loaded = loadLearningStore();
182
- if (!loaded.ok) {
183
- p.log.info(`${stateLine}\nEntries: unavailable (${formatLearningStoreUnavailable(loaded.error)})`);
184
- return;
185
- }
186
- const store = loaded.value;
187
- const { ledgerRows, logRows, rejected } = store.readLearningState(ledgerRoot);
188
- const listing = store.buildListing(ledgerRows, logRows, { rejected });
189
- p.log.info([stateLine, ...entryCountLines(listing, logRows.length, store.INACTIVE_STATUSES)].join('\n'));
190
- const { ledger, log } = listing.malformed;
191
- if (ledger + log > 0) {
192
- p.log.warn(`Malformed lines skipped: ${ledger} in the ledger, ${log} in the log. ` +
193
- 'The next op that rewrites a file moves its malformed lines to a .rejected.jsonl file beside it.');
194
- }
195
- }
196
- /**
197
- * `--list`: the store's listing — the same text json-helper's `list` op prints —
198
- * on stdout. Read-only. Outside a git project it reads the current directory.
199
- */
200
- async function handleList() {
201
- const root = (await getLedgerRoot()) ?? process.cwd();
202
- const store = requireStore();
203
- if (!store)
204
- return;
205
- const listed = store.readListing(root);
206
- if (!listed.ok) {
207
- if (listed.error.kind === 'no-learning-dir') {
208
- p.log.info(NO_LEARNING_DATA);
209
- return;
210
- }
211
- p.log.error(listed.error.message);
212
- process.exitCode = 1;
213
- return;
214
- }
215
- process.stdout.write(`${store.formatListing(listed.value)}\n`);
216
- }
217
- /**
218
- * `--show <id>`: one entry, by its anchor or its observation id, as the JSON
219
- * json-helper's `show` op prints, made terminal-safe. Read-only. Outside a git
220
- * project it reads the current directory.
221
- */
222
- async function handleShow(key) {
223
- const root = (await getLedgerRoot()) ?? process.cwd();
224
- const store = requireStore();
225
- if (!store)
226
- return;
227
- const shown = store.showByKey(root, key);
228
- if (!shown.ok) {
229
- p.log.error(shown.error.kind === 'no-learning-dir' ? NO_LEARNING_DATA : shown.error.message);
230
- process.exitCode = 1;
231
- return;
232
- }
233
- process.stdout.write(`${terminalSafeJson(shown.value)}\n`);
234
- }
235
- /**
236
- * `--restore <id>`: make an inactive entry active again through the store's
237
- * restoreAnchor, which clears its notes and its verification so maintenance
238
- * reviews it again, and re-renders the files. A refusal writes nothing.
239
- */
240
- async function handleRestore(anchor) {
241
- const ledgerRoot = await requireLedgerRoot('restore not performed');
242
- if (!ledgerRoot) {
243
- process.exitCode = 1;
244
- return;
245
- }
246
- const store = requireStore();
247
- if (!store)
248
- return;
249
- if (!store.ANCHOR_ID_RE.test(anchor)) {
250
- p.log.error(`--restore takes an entry id (ADR-NNN or PF-NNN), not ${JSON.stringify(anchor)}`);
251
- process.exitCode = 1;
252
- return;
253
- }
254
- const restored = store.restoreAnchor(ledgerRoot, anchor, { timeoutMs: LOCK_WAIT_MS });
255
- if (!restored.ok) {
256
- p.log.error(storeRefusal(restored.error, 'restored'));
257
- process.exitCode = 1;
258
- return;
259
- }
260
- p.log.success(`Restored ${restored.value.anchor_id} (${restored.value.status}); it is due for review again.`);
261
- }
262
- /**
263
- * The scope choices of `devflow learning --configure`.
264
- *
265
- * D-LEARNING-MODEL-PRECEDENCE: the session-start hook takes the first layer that
266
- * supplies a model — the project file, then a `devflow agents` Curate mapping,
267
- * then the global file. A global file therefore has no effect for a user who has
268
- * such a mapping, and its hint says so.
269
- */
270
- export const CONFIGURE_SCOPE_OPTIONS = [
271
- { value: 'project', label: 'Project', hint: 'This project only (.devflow/learning/learning.json)' },
272
- {
273
- value: 'global',
274
- label: 'Global',
275
- hint: 'All projects (~/.devflow/learning.json); a devflow agents Curate mapping takes precedence over it',
276
- },
277
- ];
278
- /** The models `devflow learning --configure` offers, each with what it is good for. */
279
- const CURATE_MODEL_CHOICES = [
280
- { value: 'opus', label: 'Opus', note: 'highest quality for detection + curation judgment' },
281
- { value: 'sonnet', label: 'Sonnet', note: 'good balance of quality and speed' },
282
- { value: 'haiku', label: 'Haiku', note: 'fastest, lowest cost' },
283
- ];
284
- /**
285
- * D-LEARNING-SHIPPED-ROW: the model offered as "Recommended" is the one the Curate
286
- * agent ships with, the `model:` line of its frontmatter. It is read from there and
287
- * written nowhere else, so a change of the shipped tier moves the recommendation with
288
- * it and this list never has to be edited. A shipped model outside the three offered
289
- * (or none) recommends nothing.
290
- */
291
- export function curateModelOptions(shippedModel) {
292
- return CURATE_MODEL_CHOICES.map(choice => ({
293
- value: choice.value,
294
- label: choice.label,
295
- hint: choice.value === shippedModel
296
- ? `Recommended — ${choice.note}`
297
- : choice.note.charAt(0).toUpperCase() + choice.note.slice(1),
298
- }));
299
- }
300
- async function handleConfigure() {
301
- p.intro(color.bgCyan(color.black(' Learning Configuration ')));
302
- const shippedModel = (await loadShippedAgentDefaults()).curate?.model;
303
- const model = await p.select({
304
- message: 'Model for decision detection',
305
- options: curateModelOptions(shippedModel),
306
- });
307
- if (p.isCancel(model)) {
308
- p.cancel('Configuration cancelled.');
309
- return;
310
- }
311
- const debugMode = await p.confirm({
312
- message: 'Enable debug logging? (logs session content excerpts to ~/.devflow/logs/)',
313
- initialValue: false,
314
- });
315
- if (p.isCancel(debugMode)) {
316
- p.cancel('Configuration cancelled.');
317
- return;
318
- }
319
- const scope = await p.select({
320
- message: 'Configuration scope',
321
- options: [...CONFIGURE_SCOPE_OPTIONS],
322
- });
323
- if (p.isCancel(scope)) {
324
- p.cancel('Configuration cancelled.');
325
- return;
326
- }
327
- const config = {
328
- model,
329
- debug: debugMode,
330
- };
331
- const configJson = JSON.stringify(config, null, 2) + '\n';
332
- if (scope === 'global') {
333
- const globalDir = getDevFlowDirectory();
334
- await fs.mkdir(globalDir, { recursive: true });
335
- await fs.writeFile(path.join(globalDir, 'learning.json'), configJson, 'utf-8');
336
- p.log.success(`Global config written to ${color.dim(path.join(globalDir, 'learning.json'))}`);
337
- }
338
- else {
339
- // D-LEDGER-MAIN-WORKTREE: session-start-context reads the project tuning config
340
- // from the ledger ($LEDGER_ROOT/.devflow/learning/), so write it there — the
341
- // main checkout in a linked worktree; the current directory outside git.
342
- const projectRoot = (await getLedgerRoot()) ?? process.cwd();
343
- const learningDir = getLearningDir(projectRoot);
344
- const projectConfigPath = getCurateTuningConfigPath(projectRoot);
345
- // D-CLI-NO-SYMLINK (core/linked-path.ts): nothing is written through a .devflow,
346
- // a learning folder or a learning.json that is a symbolic link.
347
- const linked = await firstSymbolicLink([path.dirname(learningDir), learningDir, projectConfigPath]);
348
- if (linked !== null) {
349
- p.log.error(`Project config not written: ${linked} is a symbolic link, and devflow writes nothing through one`);
350
- process.exitCode = 1;
351
- return;
352
- }
353
- await fs.mkdir(learningDir, { recursive: true });
354
- await fs.writeFile(projectConfigPath, configJson, 'utf-8');
355
- p.log.success(`Project config written to ${color.dim(projectConfigPath)}`);
356
- }
357
- p.outro(color.green('Configuration saved.'));
358
- }
359
- /**
360
- * `--reset`: remove all learning state — entries, observations, rendered files,
361
- * the tuning config, and the queue with its claim — through the store's
362
- * resetLearning, under the learning lock every learning writer takes
363
- * (D-ONE-LEARNING-LOCK, D-RESET-UNDER-LOCK). A project with no learning directory
364
- * has nothing to reset, and nothing is created there (D-NO-STRAY-TREE).
365
- */
366
- async function handleReset() {
367
- const ledgerRoot = await requireLedgerRoot('reset not performed');
368
- if (!ledgerRoot)
369
- return;
370
- const store = requireStore();
371
- if (!store)
372
- return;
373
- if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
374
- p.log.info('No learning data to reset.');
375
- return;
376
- }
377
- const proceed = await confirmOnTerminal('Remove all learning state files? This cannot be undone.', 'Reset cancelled.');
378
- if (!proceed)
379
- return;
380
- const reset = store.resetLearning(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
381
- if (!reset.ok) {
382
- if (reset.error.kind === 'no-learning-dir') {
383
- p.log.info('No learning data to reset.');
384
- return;
385
- }
386
- p.log.error(storeRefusal(reset.error, 'reset'));
387
- process.exitCode = 1;
388
- return;
389
- }
390
- p.log.success('Reset complete — removed .devflow/learning/ state.');
391
- }
392
- /**
393
- * `--clear`: drop the observations no entry uses (D-CLEAR-UNREFERENCED), then
394
- * drain the learning queue. The queue is drained only once the clear succeeded:
395
- * a busy lock or a refusal leaves both the log and the queue as they were, so a
396
- * failed clear changes nothing.
397
- */
398
- async function handleClear() {
399
- const ledgerRoot = await requireLedgerRoot('clear not performed');
400
- if (!ledgerRoot)
401
- return;
402
- const store = requireStore();
403
- if (!store)
404
- return;
405
- if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
406
- p.log.info('No learning data to clear.');
407
- return;
408
- }
409
- const proceed = await confirmOnTerminal('Drop every observation no entry uses? Entries and the observations they use are kept. This cannot be undone.', 'Clear cancelled.');
410
- if (!proceed)
411
- return;
412
- const cleared = store.clearUnreferenced(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
413
- if (!cleared.ok) {
414
- if (cleared.error.kind === 'no-learning-dir') {
415
- p.log.info('No learning data to clear.');
416
- return;
417
- }
418
- p.log.error(storeRefusal(cleared.error, 'cleared'));
419
- process.exitCode = 1;
420
- return;
421
- }
422
- // A mid-run Curate agent whose claimed batch vanishes stops without further
423
- // writes — the desired outcome of clearing.
424
- const drain = await drainLearningQueue(ledgerRoot);
425
- p.log.success(`Cleared ${counted(cleared.value.cleared, 'observation')} no entry uses and kept ${cleared.value.kept} ` +
426
- `that entries use${drain.drained ? '; drained the learning queue.' : '.'}`);
427
- if (!drain.drained)
428
- p.log.warn(formatRefusedDrain('learning', drain.linkedFolder));
429
- }
430
- /**
431
- * Make the installed prompts match the switch just written
432
- * (D-LEARNING-VARIANT-INSTALL): the learning-on or learning-off commands and
433
- * agents, and the apply-decisions skill. The same convergence `devflow init` runs,
434
- * followed by reapplying the saved agent model mapping so `devflow agents`
435
- * overrides survive.
436
- *
437
- * Warn-not-fail: the switch itself is already recorded, and either mismatch is
438
- * safe (an on variant is still gated at run time, an off variant loads no
439
- * decisions), so a problem here never changes the exit code.
440
- */
441
- async function convergeInstalledVariants(enabled) {
442
- try {
443
- const outcome = await applyLearningToggle({
444
- claudeDir: getClaudeDirectory(),
445
- devflowDir: getDevFlowDirectory(),
446
- learning: enabled,
447
- runningVersion: await readRunningVersion(),
448
- warn: (msg) => p.log.warn(msg),
449
- });
450
- if (outcome.kind === 'skipped') {
451
- p.log.warn(outcome.message);
452
- return;
453
- }
454
- const changed = describeLearningConverge(outcome.result, enabled);
455
- if (changed !== null)
456
- p.log.info(color.dim(`Installed prompts: ${changed}`));
457
- }
458
- catch (err) {
459
- p.log.warn(`Could not update the installed prompts for the new learning setting — run ${color.cyan('devflow init')}: ${String(err)}`);
460
- }
461
- }
462
- /**
463
- * `--enable` / `--disable`: the machine-wide switch (D-FEATURES-NARROW-ONLY),
464
- * converged exactly as `devflow init --learning / --no-learning` converges it —
465
- * the manifest value, the installed prompt variants and skill
466
- * (D-LEARNING-VARIANT-INSTALL), and on disable a drained queue in the current
467
- * project. Never requires a git root: the switch is not a per-project setting.
468
- */
469
- async function handleToggle(enabled) {
470
- const recorded = await writeMachineFeature(getDevFlowDirectory(), 'learning', enabled);
471
- if (!recorded.ok) {
472
- p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
473
- process.exitCode = 1;
474
- return;
475
- }
476
- await convergeInstalledVariants(enabled);
477
- if (enabled) {
478
- p.log.success('Learning enabled in every project (a repository can opt out)');
479
- p.log.info(color.dim('Architectural decisions and pitfalls will be detected from your sessions'));
480
- return;
481
- }
482
- // Drain the current project's learning (decisions-detection) queue so stale
483
- // turns don't process on re-enable. A mid-run Curate agent whose claimed
484
- // batch vanishes aborts without changes — the desired outcome of disabling.
485
- const ledgerRoot = await getLedgerRoot();
486
- if (ledgerRoot) {
487
- const drain = await drainLearningQueue(ledgerRoot);
488
- if (!drain.drained)
489
- p.log.warn(formatRefusedDrain('learning', drain.linkedFolder));
490
- }
491
- p.log.success('Learning disabled in every project');
492
- }
493
- export const learningCommand = new Command('learning')
494
- .description('Enable or disable learning (decision/pitfall detection) in every project')
495
- .option('--enable', 'Enable learning in every project (a repository can opt out)')
496
- .option('--disable', 'Disable learning in every project')
497
- .option('--status', 'Show learning status and entry counts')
498
- .option('--list', 'List entries, inactive entries with their notes, and observations')
499
- .option('--show <id>', 'Print one entry (ADR-NNN or PF-NNN) or observation (obs_...) as JSON')
500
- .option('--restore <id>', 'Make an inactive entry active again')
501
- .option('--configure', 'Interactive configuration wizard for learning.json')
502
- .option('--clear', 'Drop the observations no entry uses, and drain the learning queue')
503
- .option('--reset', 'Remove all learning state files and artifacts')
504
- .action(async (options) => {
505
- const knownFlags = [
506
- 'enable', 'disable', 'status', 'list', 'show', 'restore', 'configure',
507
- 'clear', 'reset',
508
- ];
509
- const hasFlag = knownFlags.some((f) => options[f] !== undefined);
510
- if (!hasFlag) {
511
- printUsage();
512
- return;
513
- }
514
- // Thin router — the read-only commands first (status, list, show), then
515
- // configure, reset, clear, restore, enable, disable. Each handler owns its own
516
- // path resolution and I/O.
517
- if (options.status) {
518
- await handleStatus();
519
- return;
520
- }
521
- if (options.list) {
522
- await handleList();
523
- return;
524
- }
525
- if (options.show !== undefined) {
526
- await handleShow(options.show);
527
- return;
528
- }
529
- if (options.configure) {
530
- await handleConfigure();
531
- return;
532
- }
533
- if (options.reset) {
534
- await handleReset();
535
- return;
536
- }
537
- if (options.clear) {
538
- await handleClear();
539
- return;
540
- }
541
- if (options.restore !== undefined) {
542
- await handleRestore(options.restore);
543
- return;
544
- }
545
- if (options.enable) {
546
- await handleToggle(true);
547
- return;
548
- }
549
- if (options.disable) {
550
- await handleToggle(false);
551
- return;
552
- }
553
- });
554
- //# sourceMappingURL=learning.js.map
@@ -1,27 +0,0 @@
1
- /**
2
- * @file learning-queue-cleanup.ts
3
- *
4
- * `drainLearningQueue`, the learning queue drain shared by `devflow learning
5
- * --clear` and `--disable` (src/cli/commands/learning.ts) and by the drain
6
- * `devflow init` runs when learning is switched off (src/cli/commands/init.ts).
7
- */
8
- import * as path from 'path';
9
- import { getLearningClaimOwnerPath, getLearningDir, getLearningPendingTurnsPath, getLearningPendingTurnsProcessingPath, } from './project-paths.js';
10
- import { drainQueueFiles } from './queue-drain.js';
11
- /**
12
- * Drain the learning (decisions-detection) pending-turns queue, its claimed
13
- * batch and the claim's owner file so stale turns don't process later — used by
14
- * both `--clear` and `--disable`. A mid-run Curate agent whose claimed batch
15
- * vanishes aborts without changes, which is the desired outcome in both cases.
16
- * Refused, deleting nothing, when `.devflow` or `.devflow/learning` under `gitRoot`
17
- * is a symbolic link (D-CLI-NO-SYMLINK). ENOENT-tolerant; other errors propagate.
18
- */
19
- export async function drainLearningQueue(gitRoot) {
20
- const learningDir = getLearningDir(gitRoot);
21
- return drainQueueFiles([path.dirname(learningDir), learningDir], [
22
- getLearningPendingTurnsPath(gitRoot),
23
- getLearningPendingTurnsProcessingPath(gitRoot),
24
- getLearningClaimOwnerPath(gitRoot),
25
- ]);
26
- }
27
- //# sourceMappingURL=learning-queue-cleanup.js.map
@@ -1,61 +0,0 @@
1
- /**
2
- * The CLI's view of the learning store — a typed seam onto the package's own
3
- * `hooks/lib/learning-store.cjs`, never a second implementation of it.
4
- *
5
- * D-LEARNING-STORE-SEAM: `devflow learning` reads and writes the learning tree only
6
- * through the store module json-helper's ops and the hooks run, loaded with the
7
- * evidence-policy seam's `loadScript` from the package's own scripts directory and
8
- * shape-checked against LEARNING_STORE_SURFACE; the interfaces below are
9
- * TRANSCRIBED from the store's JSDoc and are this side's only shape authority.
10
- * Reason: the CLI's own TypeScript reader judged every row by the v1 shape, so it
11
- * counted v2 observations as invalid, and its `--clear` truncated the log without
12
- * the lock every learning writer takes.
13
- *
14
- * As with the evidence resolver (D-POLICY-CJS-SEAM), the CLI loads the package
15
- * copy, which `npm` installs with this CLI, not `~/.devflow/scripts`, which
16
- * `devflow init` refreshes: the two can differ until the next init.
17
- */
18
- import { join } from 'path';
19
- import { scriptsDir } from './assets.js';
20
- import { loadScript } from './evidence-policy.js';
21
- /** The store, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
22
- export const LEARNING_STORE_SCRIPT_NAME = join('hooks', 'lib', 'learning-store.cjs');
23
- /**
24
- * Every key of LearningStoreModule and the runtime kind the loader requires of
25
- * it. `satisfies` makes the compiler reject an interface key missing here.
26
- */
27
- export const LEARNING_STORE_SURFACE = Object.freeze({
28
- INACTIVE_STATUSES: 'string-array',
29
- ANCHOR_ID_RE: 'regexp',
30
- readLearningState: 'function',
31
- buildListing: 'function',
32
- readListing: 'function',
33
- formatListing: 'function',
34
- showByKey: 'function',
35
- restoreAnchor: 'function',
36
- clearUnreferenced: 'function',
37
- resetLearning: 'function',
38
- });
39
- /**
40
- * Load the learning store from `dir` (default: the package's own scripts
41
- * directory) and shape-check its surface. Never throws: a missing file is
42
- * `not-found`; a module that throws on load or lacks a surface key is `unusable`.
43
- */
44
- export function loadLearningStore(dir = scriptsDir()) {
45
- return loadScript(join(dir, LEARNING_STORE_SCRIPT_NAME), LEARNING_STORE_SURFACE);
46
- }
47
- /**
48
- * Why the store cannot be used, and the remedy: a package reinstall, since the
49
- * CLI loads the package's own copy, which `devflow init` does not restore.
50
- */
51
- export function formatLearningStoreUnavailable(error) {
52
- switch (error.kind) {
53
- case 'not-found': return 'learning store not found — reinstall devflow-kit';
54
- case 'unusable': return 'learning store failed to load — reinstall devflow-kit';
55
- default: {
56
- const exhaustive = error;
57
- return exhaustive;
58
- }
59
- }
60
- }
61
- //# sourceMappingURL=learning-store.js.map