devflow-kit 3.0.1 → 3.1.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.
- package/CHANGELOG.md +13 -0
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +2 -2
- package/dist/cli/agents-view/state.js +2 -2
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +7 -6
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +34 -32
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +267 -129
- package/dist/cli/commands/memory.js +1 -1
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +20 -20
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/dynamic-build.md +18 -4
- package/dist/commands/dynamic-plan.md +19 -5
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/release.md +15 -1
- package/dist/commands/research.md +1 -1
- package/dist/commands/resolve.md +8 -9
- package/dist/core/agent-frontmatter.js +3 -3
- package/dist/core/agent-models.js +6 -6
- package/dist/core/agent-state.js +2 -2
- package/dist/core/ansi.js +2 -2
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +3 -3
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +25 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -80
- package/dist/core/learning-store.js +61 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +4 -5
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +2 -16
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +24 -24
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +7 -7
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +1 -4
- package/src/assets/agents/design.md +2 -2
- package/src/assets/agents/diagnose.md +1 -1
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +1 -1
- package/src/assets/agents/review.md +3 -3
- package/src/assets/agents/scrutinize.md +1 -1
- package/src/assets/agents/skim.md +1 -1
- package/src/assets/agents/triage.md +9 -9
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +1 -1
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/dynamic-build.mds +1 -1
- package/src/assets/commands/dynamic-plan.mds +3 -3
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/release.md +15 -1
- package/src/assets/commands/research.mds +1 -1
- package/src/assets/commands/resolve.mds +8 -9
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +1 -1
- package/src/assets/mds/tracker/_jira.mds +1 -1
- package/src/assets/mds/tracker/_linear.mds +1 -1
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -0
- package/src/assets/scripts/hooks/background-memory-update +28 -22
- package/src/assets/scripts/hooks/capture-turn +1 -17
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +1 -1
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +348 -814
- package/src/assets/scripts/hooks/json-parse +3 -2
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3102 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/queue-append +2 -2
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +40 -18
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -3,25 +3,117 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
|
-
import { getLearningDir, getLearningTuningConfigPath,
|
|
6
|
+
import { getLearningDir, getLearningTuningConfigPath, } from '../../core/project-paths.js';
|
|
7
7
|
import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
|
|
8
8
|
import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
|
|
9
9
|
import { getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
10
10
|
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
|
|
12
|
+
import { formatLearningStoreUnavailable, loadLearningStore, } from '../../core/learning-store.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** What the read and restore commands say about a project with no `.devflow/learning/`. */
|
|
17
|
+
const NO_LEARNING_DATA = 'No learning data in this project yet.';
|
|
18
|
+
/**
|
|
19
|
+
* How long a writer here (--restore, --clear, --reset) waits for the learning lock
|
|
20
|
+
* before it refuses as busy. An op holds the lock for milliseconds, so a longer
|
|
21
|
+
* wait means a stuck holder, and the store's own 30 s wait would leave the command
|
|
22
|
+
* looking hung.
|
|
23
|
+
*/
|
|
24
|
+
const LOCK_WAIT_MS = 5000;
|
|
25
|
+
/**
|
|
26
|
+
* Code point ranges JSON.stringify leaves raw that a terminal may act on, or may
|
|
27
|
+
* display reordered: DEL and the C1 controls, the directional marks, the line and
|
|
28
|
+
* paragraph separators, and the bidirectional embeddings, overrides and isolates.
|
|
29
|
+
*/
|
|
30
|
+
const TERMINAL_UNSAFE_RANGES = [
|
|
31
|
+
[0x7f, 0x9f],
|
|
32
|
+
[0x200e, 0x200f],
|
|
33
|
+
[0x2028, 0x2029],
|
|
34
|
+
[0x202a, 0x202e],
|
|
35
|
+
[0x2066, 0x2069],
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* `value` as pretty JSON a terminal shows as written: every character in
|
|
39
|
+
* TERMINAL_UNSAFE_RANGES becomes its `\uXXXX` escape. Such characters can occur
|
|
40
|
+
* only inside JSON strings, where the escape means the same character, so the
|
|
41
|
+
* text still parses back to `value`.
|
|
42
|
+
*/
|
|
43
|
+
function terminalSafeJson(value) {
|
|
44
|
+
let text = '';
|
|
45
|
+
for (const ch of JSON.stringify(value, null, 2)) {
|
|
46
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
47
|
+
const unsafe = TERMINAL_UNSAFE_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
48
|
+
text += unsafe ? `\\u${code.toString(16).padStart(4, '0')}` : ch;
|
|
49
|
+
}
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
/** `1 decision`, `2 decisions`. */
|
|
53
|
+
function counted(count, noun) {
|
|
54
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The learning store (D-LEARNING-STORE-SEAM), or null after reporting why it
|
|
58
|
+
* cannot be used and setting exit code 1.
|
|
59
|
+
*/
|
|
60
|
+
function requireStore() {
|
|
61
|
+
const loaded = loadLearningStore();
|
|
62
|
+
if (loaded.ok)
|
|
63
|
+
return loaded.value;
|
|
64
|
+
p.log.error(`Learning: ${formatLearningStoreUnavailable(loaded.error)}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** What a writer says when the store refused; `undone` completes "Nothing was …". */
|
|
69
|
+
function storeRefusal(error, undone) {
|
|
70
|
+
switch (error.kind) {
|
|
71
|
+
case 'no-learning-dir': return NO_LEARNING_DATA;
|
|
72
|
+
case 'busy': return `The learning store is busy: another run holds its lock. Nothing was ${undone}; try again in a moment.`;
|
|
73
|
+
default: return error.message;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Ask `message` on a terminal and say whether to go on, logging `cancelled` when the
|
|
78
|
+
* answer is no. With no terminal there is no one to ask: a script that passed the
|
|
79
|
+
* flag has decided.
|
|
80
|
+
*/
|
|
81
|
+
async function confirmOnTerminal(message, cancelled) {
|
|
82
|
+
if (!process.stdin.isTTY)
|
|
83
|
+
return true;
|
|
84
|
+
const confirmed = await p.confirm({ message, initialValue: false });
|
|
85
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
86
|
+
p.log.info(cancelled);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/** True when `dir` is a directory; false when nothing, or something else, is there. */
|
|
92
|
+
async function isDirectory(dir) {
|
|
93
|
+
try {
|
|
94
|
+
return (await fs.stat(dir)).isDirectory();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const code = err.code;
|
|
98
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
99
|
+
return false;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
13
103
|
// ---------------------------------------------------------------------------
|
|
14
104
|
// Sub-command handlers
|
|
15
105
|
// ---------------------------------------------------------------------------
|
|
16
106
|
function printUsage() {
|
|
17
107
|
p.intro(color.bgCyan(color.black(' Learning ')));
|
|
18
|
-
p.note(`${color.cyan('devflow learning --enable')}
|
|
19
|
-
`${color.cyan('devflow learning --disable')}
|
|
20
|
-
`${color.cyan('devflow learning --status')}
|
|
21
|
-
`${color.cyan('devflow learning --list')}
|
|
22
|
-
`${color.cyan('devflow learning --
|
|
23
|
-
`${color.cyan('devflow learning --
|
|
24
|
-
`${color.cyan('devflow learning --
|
|
108
|
+
p.note(`${color.cyan('devflow learning --enable')} Enable learning in every project (a repository can opt out)\n` +
|
|
109
|
+
`${color.cyan('devflow learning --disable')} Disable learning in every project (drains this project's queue)\n` +
|
|
110
|
+
`${color.cyan('devflow learning --status')} Show learning status and entry counts\n` +
|
|
111
|
+
`${color.cyan('devflow learning --list')} List entries, inactive entries and observations\n` +
|
|
112
|
+
`${color.cyan('devflow learning --show <id>')} Print one entry or observation as JSON\n` +
|
|
113
|
+
`${color.cyan('devflow learning --restore <id>')} Make an inactive entry active again\n` +
|
|
114
|
+
`${color.cyan('devflow learning --configure')} Configuration wizard\n` +
|
|
115
|
+
`${color.cyan('devflow learning --clear')} Drop the observations no entry uses, and drain the queue\n` +
|
|
116
|
+
`${color.cyan('devflow learning --reset')} Remove all learning state files`, 'Usage');
|
|
25
117
|
p.outro(color.dim('Detects architectural decisions and known pitfalls from your sessions'));
|
|
26
118
|
}
|
|
27
119
|
/**
|
|
@@ -40,10 +132,35 @@ async function requireLedgerRoot(actionSuffix) {
|
|
|
40
132
|
}
|
|
41
133
|
return ledgerRoot;
|
|
42
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The --status entry lines: active entries by type, inactive ones by status (in
|
|
137
|
+
* the store's order, each status present), the active entries still in the v1
|
|
138
|
+
* format — the migration still to do — and the observations.
|
|
139
|
+
*/
|
|
140
|
+
function entryCountLines(listing, logRowCount, inactiveStatuses) {
|
|
141
|
+
const active = listing.active;
|
|
142
|
+
const decisions = active.filter(entry => entry.type === 'decision').length;
|
|
143
|
+
const pitfalls = active.filter(entry => entry.type === 'pitfall').length;
|
|
144
|
+
const byStatus = inactiveStatuses
|
|
145
|
+
.map(status => ({ status, count: listing.inactive.filter(entry => entry.status === status).length }))
|
|
146
|
+
.filter(({ count }) => count > 0)
|
|
147
|
+
.map(({ status, count }) => `${status} ${count}`);
|
|
148
|
+
const legacy = active.filter(entry => entry.schema === 1).length;
|
|
149
|
+
return [
|
|
150
|
+
`Entries: ${active.length} active (${counted(decisions, 'decision')}, ${counted(pitfalls, 'pitfall')}), `
|
|
151
|
+
+ `${listing.inactive.length} inactive${byStatus.length > 0 ? ` (${byStatus.join(', ')})` : ''}`,
|
|
152
|
+
`Legacy v1 entries: ${legacy} of ${active.length} active`,
|
|
153
|
+
`Observations: ${logRowCount} in the log, ${listing.observations.length} not yet promoted`,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `--status`: the machine switch, then the counts the store reads. Reads only:
|
|
158
|
+
* malformed lines are counted, never quarantined, and no scope is checked.
|
|
159
|
+
*/
|
|
43
160
|
async function handleStatus() {
|
|
44
161
|
// D-FEATURES-NARROW-ONLY: the machine switch is the manifest's and reads the
|
|
45
162
|
// same from every directory; a repository layer can only narrow it, and adds a
|
|
46
|
-
// line only when it does. The
|
|
163
|
+
// line only when it does. The entry counts are per-project.
|
|
47
164
|
const enabled = await readMachineFeature(getDevFlowDirectory(), 'learning');
|
|
48
165
|
const settingsModule = loadSettingsModule();
|
|
49
166
|
const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'learning') : null;
|
|
@@ -54,68 +171,89 @@ async function handleStatus() {
|
|
|
54
171
|
p.log.warn(trackedWarning);
|
|
55
172
|
const ledgerRoot = await getLedgerRoot();
|
|
56
173
|
if (!ledgerRoot) {
|
|
57
|
-
p.log.info(`${stateLine}\
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const logPath = getDecisionsLogPath(ledgerRoot);
|
|
61
|
-
const { observations, invalidCount } = await readObservations(logPath);
|
|
62
|
-
const decisionObs = observations.filter(o => o.type === 'decision' || o.type === 'pitfall');
|
|
63
|
-
const decisions = observations.filter(o => o.type === 'decision');
|
|
64
|
-
const pitfalls = observations.filter(o => o.type === 'pitfall');
|
|
65
|
-
const created = decisionObs.filter(o => o.status === 'created');
|
|
66
|
-
const ready = decisionObs.filter(o => o.status === 'ready');
|
|
67
|
-
const observing = decisionObs.filter(o => o.status === 'observing');
|
|
68
|
-
const deprecated = decisionObs.filter(o => o.status === 'deprecated');
|
|
69
|
-
const lines = [stateLine];
|
|
70
|
-
if (decisionObs.length === 0) {
|
|
71
|
-
lines.push('Observations: none');
|
|
174
|
+
p.log.info(`${stateLine}\nEntries: not in a git project`);
|
|
175
|
+
return;
|
|
72
176
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
177
|
+
const loaded = loadLearningStore();
|
|
178
|
+
if (!loaded.ok) {
|
|
179
|
+
p.log.info(`${stateLine}\nEntries: unavailable (${formatLearningStoreUnavailable(loaded.error)})`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const store = loaded.value;
|
|
183
|
+
const { ledgerRows, logRows, rejected } = store.readLearningState(ledgerRoot);
|
|
184
|
+
const listing = store.buildListing(ledgerRows, logRows, { rejected });
|
|
185
|
+
p.log.info([stateLine, ...entryCountLines(listing, logRows.length, store.INACTIVE_STATUSES)].join('\n'));
|
|
186
|
+
const { ledger, log } = listing.malformed;
|
|
187
|
+
if (ledger + log > 0) {
|
|
188
|
+
p.log.warn(`Malformed lines skipped: ${ledger} in the ledger, ${log} in the log. ` +
|
|
189
|
+
'The next op that rewrites a file moves its malformed lines to a .rejected.jsonl file beside it.');
|
|
77
190
|
}
|
|
78
|
-
p.log.info(lines.join('\n'));
|
|
79
|
-
warnIfInvalid(invalidCount);
|
|
80
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* `--list`: the store's listing — the same text json-helper's `list` op prints —
|
|
194
|
+
* on stdout. Read-only. Outside a git project it reads the current directory.
|
|
195
|
+
*/
|
|
81
196
|
async function handleList() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
197
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
198
|
+
const store = requireStore();
|
|
199
|
+
if (!store)
|
|
200
|
+
return;
|
|
201
|
+
const listed = store.readListing(root);
|
|
202
|
+
if (!listed.ok) {
|
|
203
|
+
if (listed.error.kind === 'no-learning-dir') {
|
|
204
|
+
p.log.info(NO_LEARNING_DATA);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
p.log.error(listed.error.message);
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
91
210
|
}
|
|
92
|
-
|
|
93
|
-
|
|
211
|
+
process.stdout.write(`${store.formatListing(listed.value)}\n`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* `--show <id>`: one entry, by its anchor or its observation id, as the JSON
|
|
215
|
+
* json-helper's `show` op prints, made terminal-safe. Read-only. Outside a git
|
|
216
|
+
* project it reads the current directory.
|
|
217
|
+
*/
|
|
218
|
+
async function handleShow(key) {
|
|
219
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
220
|
+
const store = requireStore();
|
|
221
|
+
if (!store)
|
|
222
|
+
return;
|
|
223
|
+
const shown = store.showByKey(root, key);
|
|
224
|
+
if (!shown.ok) {
|
|
225
|
+
p.log.error(shown.error.kind === 'no-learning-dir' ? NO_LEARNING_DATA : shown.error.message);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
return;
|
|
94
228
|
}
|
|
95
|
-
|
|
96
|
-
|
|
229
|
+
process.stdout.write(`${terminalSafeJson(shown.value)}\n`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* `--restore <id>`: make an inactive entry active again through the store's
|
|
233
|
+
* restoreAnchor, which clears its notes and its verification so maintenance
|
|
234
|
+
* reviews it again, and re-renders the files. A refusal writes nothing.
|
|
235
|
+
*/
|
|
236
|
+
async function handleRestore(anchor) {
|
|
237
|
+
const ledgerRoot = await requireLedgerRoot('restore not performed');
|
|
238
|
+
if (!ledgerRoot) {
|
|
239
|
+
process.exitCode = 1;
|
|
97
240
|
return;
|
|
98
241
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
242
|
+
const store = requireStore();
|
|
243
|
+
if (!store)
|
|
244
|
+
return;
|
|
245
|
+
if (!store.ANCHOR_ID_RE.test(anchor)) {
|
|
246
|
+
p.log.error(`--restore takes an entry id (ADR-NNN or PF-NNN), not ${JSON.stringify(anchor)}`);
|
|
247
|
+
process.exitCode = 1;
|
|
103
248
|
return;
|
|
104
249
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const statusIcon = obs.status === 'created' ? color.green('created')
|
|
111
|
-
: obs.status === 'ready' ? color.yellow('ready')
|
|
112
|
-
: obs.status === 'deprecated' ? color.dim('deprecated')
|
|
113
|
-
: color.dim('observing');
|
|
114
|
-
const conf = (obs.confidence * 100).toFixed(0);
|
|
115
|
-
p.log.info(`[${typeIcon}] ${color.cyan(obs.pattern)} (${conf}% | ${obs.observations}x | ${statusIcon})`);
|
|
250
|
+
const restored = store.restoreAnchor(ledgerRoot, anchor, { timeoutMs: LOCK_WAIT_MS });
|
|
251
|
+
if (!restored.ok) {
|
|
252
|
+
p.log.error(storeRefusal(restored.error, 'restored'));
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
return;
|
|
116
255
|
}
|
|
117
|
-
|
|
118
|
-
p.outro(color.dim(`${filtered.length} observation(s) total`));
|
|
256
|
+
p.log.success(`Restored ${restored.value.anchor_id} (${restored.value.status}); it is due for review again.`);
|
|
119
257
|
}
|
|
120
258
|
async function handleConfigure() {
|
|
121
259
|
p.intro(color.bgCyan(color.black(' Learning Configuration ')));
|
|
@@ -173,84 +311,74 @@ async function handleConfigure() {
|
|
|
173
311
|
}
|
|
174
312
|
p.outro(color.green('Configuration saved.'));
|
|
175
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* `--reset`: remove all learning state — entries, observations, rendered files,
|
|
316
|
+
* the tuning config, and the queue with its claim — through the store's
|
|
317
|
+
* resetLearning, under the learning lock every learning writer takes
|
|
318
|
+
* (D-ONE-LEARNING-LOCK, D-RESET-UNDER-LOCK). A project with no learning directory
|
|
319
|
+
* has nothing to reset, and nothing is created there (D-NO-STRAY-TREE).
|
|
320
|
+
*/
|
|
176
321
|
async function handleReset() {
|
|
177
322
|
const ledgerRoot = await requireLedgerRoot('reset not performed');
|
|
178
323
|
if (!ledgerRoot)
|
|
179
324
|
return;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await
|
|
184
|
-
|
|
185
|
-
// Non-recursive: EEXIST still means genuine contention.
|
|
186
|
-
try {
|
|
187
|
-
await fs.mkdir(lockDir);
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
p.log.error('Learning system is currently running. Try again in a moment.');
|
|
325
|
+
const store = requireStore();
|
|
326
|
+
if (!store)
|
|
327
|
+
return;
|
|
328
|
+
if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
|
|
329
|
+
p.log.info('No learning data to reset.');
|
|
191
330
|
return;
|
|
192
331
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// Remove the entire learning directory (contains queue files, content files,
|
|
205
|
-
// ledger, and tuning config). Single-dir semantics: all learning state lives here.
|
|
206
|
-
try {
|
|
207
|
-
await fs.rm(getLearningDir(ledgerRoot), { recursive: true, force: true });
|
|
208
|
-
}
|
|
209
|
-
catch { /* best effort */ }
|
|
210
|
-
// Clean legacy dream marker-pipeline stamps from old installs.
|
|
211
|
-
// Best-effort: sweeps the now-absent dir silently (ENOENT-tolerant).
|
|
212
|
-
try {
|
|
213
|
-
await sweepLegacyDreamMarkers(getLearningDir(ledgerRoot));
|
|
214
|
-
}
|
|
215
|
-
catch { /* best effort */ }
|
|
216
|
-
p.log.success('Reset complete — removed .devflow/learning/ state.');
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
try {
|
|
220
|
-
await fs.rmdir(lockDir);
|
|
332
|
+
const proceed = await confirmOnTerminal('Remove all learning state files? This cannot be undone.', 'Reset cancelled.');
|
|
333
|
+
if (!proceed)
|
|
334
|
+
return;
|
|
335
|
+
const reset = store.resetLearning(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
336
|
+
if (!reset.ok) {
|
|
337
|
+
if (reset.error.kind === 'no-learning-dir') {
|
|
338
|
+
p.log.info('No learning data to reset.');
|
|
339
|
+
return;
|
|
221
340
|
}
|
|
222
|
-
|
|
341
|
+
p.log.error(storeRefusal(reset.error, 'reset'));
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
return;
|
|
223
344
|
}
|
|
345
|
+
p.log.success('Reset complete — removed .devflow/learning/ state.');
|
|
224
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* `--clear`: drop the observations no entry uses (D-CLEAR-UNREFERENCED), then
|
|
349
|
+
* drain the learning queue. The queue is drained only once the clear succeeded:
|
|
350
|
+
* a busy lock or a refusal leaves both the log and the queue as they were, so a
|
|
351
|
+
* failed clear changes nothing.
|
|
352
|
+
*/
|
|
225
353
|
async function handleClear() {
|
|
226
354
|
const ledgerRoot = await requireLedgerRoot('clear not performed');
|
|
227
355
|
if (!ledgerRoot)
|
|
228
356
|
return;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
p.log.info('No decisions log to clear.');
|
|
357
|
+
const store = requireStore();
|
|
358
|
+
if (!store)
|
|
359
|
+
return;
|
|
360
|
+
if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
|
|
361
|
+
p.log.info('No learning data to clear.');
|
|
235
362
|
return;
|
|
236
363
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
p.log.info('
|
|
364
|
+
const proceed = await confirmOnTerminal('Drop every observation no entry uses? Entries and the observations they use are kept. This cannot be undone.', 'Clear cancelled.');
|
|
365
|
+
if (!proceed)
|
|
366
|
+
return;
|
|
367
|
+
const cleared = store.clearUnreferenced(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
368
|
+
if (!cleared.ok) {
|
|
369
|
+
if (cleared.error.kind === 'no-learning-dir') {
|
|
370
|
+
p.log.info('No learning data to clear.');
|
|
244
371
|
return;
|
|
245
372
|
}
|
|
373
|
+
p.log.error(storeRefusal(cleared.error, 'cleared'));
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
return;
|
|
246
376
|
}
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
// on the next session — mirrors memory.ts's drain-on-disable behavior for
|
|
250
|
-
// the sibling memory queue. A mid-run Learning agent whose claimed batch
|
|
251
|
-
// vanishes aborts without changes — the desired outcome of clearing.
|
|
377
|
+
// A mid-run Learning agent whose claimed batch vanishes stops without further
|
|
378
|
+
// writes — the desired outcome of clearing.
|
|
252
379
|
await drainLearningQueue(ledgerRoot);
|
|
253
|
-
p.log.success('
|
|
380
|
+
p.log.success(`Cleared ${counted(cleared.value.cleared, 'observation')} no entry uses and kept ${cleared.value.kept} ` +
|
|
381
|
+
'that entries use; drained the learning queue.');
|
|
254
382
|
}
|
|
255
383
|
/**
|
|
256
384
|
* `--enable` / `--disable`: the machine-wide switch (D-FEATURES-NARROW-ONLY),
|
|
@@ -283,24 +411,26 @@ export const learningCommand = new Command('learning')
|
|
|
283
411
|
.description('Enable or disable learning (decision/pitfall detection) in every project')
|
|
284
412
|
.option('--enable', 'Enable learning in every project (a repository can opt out)')
|
|
285
413
|
.option('--disable', 'Disable learning in every project')
|
|
286
|
-
.option('--status', 'Show learning status and
|
|
287
|
-
.option('--list', '
|
|
414
|
+
.option('--status', 'Show learning status and entry counts')
|
|
415
|
+
.option('--list', 'List entries, inactive entries with their notes, and observations')
|
|
416
|
+
.option('--show <id>', 'Print one entry (ADR-NNN or PF-NNN) or observation (obs_...) as JSON')
|
|
417
|
+
.option('--restore <id>', 'Make an inactive entry active again')
|
|
288
418
|
.option('--configure', 'Interactive configuration wizard for learning.json')
|
|
289
|
-
.option('--clear', '
|
|
419
|
+
.option('--clear', 'Drop the observations no entry uses, and drain the learning queue')
|
|
290
420
|
.option('--reset', 'Remove all learning state files and artifacts')
|
|
291
421
|
.action(async (options) => {
|
|
292
422
|
const knownFlags = [
|
|
293
|
-
'enable', 'disable', 'status', 'list', 'configure',
|
|
423
|
+
'enable', 'disable', 'status', 'list', 'show', 'restore', 'configure',
|
|
294
424
|
'clear', 'reset',
|
|
295
425
|
];
|
|
296
|
-
const hasFlag = knownFlags.some((f) => options[f]);
|
|
426
|
+
const hasFlag = knownFlags.some((f) => options[f] !== undefined);
|
|
297
427
|
if (!hasFlag) {
|
|
298
428
|
printUsage();
|
|
299
429
|
return;
|
|
300
430
|
}
|
|
301
|
-
// Thin router —
|
|
302
|
-
//
|
|
303
|
-
//
|
|
431
|
+
// Thin router — the read-only commands first (status, list, show), then
|
|
432
|
+
// configure, reset, clear, restore, enable, disable. Each handler owns its own
|
|
433
|
+
// path resolution and I/O.
|
|
304
434
|
if (options.status) {
|
|
305
435
|
await handleStatus();
|
|
306
436
|
return;
|
|
@@ -309,6 +439,10 @@ export const learningCommand = new Command('learning')
|
|
|
309
439
|
await handleList();
|
|
310
440
|
return;
|
|
311
441
|
}
|
|
442
|
+
if (options.show !== undefined) {
|
|
443
|
+
await handleShow(options.show);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
312
446
|
if (options.configure) {
|
|
313
447
|
await handleConfigure();
|
|
314
448
|
return;
|
|
@@ -321,6 +455,10 @@ export const learningCommand = new Command('learning')
|
|
|
321
455
|
await handleClear();
|
|
322
456
|
return;
|
|
323
457
|
}
|
|
458
|
+
if (options.restore !== undefined) {
|
|
459
|
+
await handleRestore(options.restore);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
324
462
|
if (options.enable) {
|
|
325
463
|
await handleToggle(true);
|
|
326
464
|
return;
|
|
@@ -83,7 +83,7 @@ export function addMemoryHooks(settingsJson, devflowDir) {
|
|
|
83
83
|
export function removeMemoryHooks(input) {
|
|
84
84
|
const settingsJson = typeof input === 'string' ? input : JSON.stringify(input);
|
|
85
85
|
const settings = typeof input === 'string' ? JSON.parse(input) : structuredClone(input);
|
|
86
|
-
// Evaluate every removal into a local — never short-circuit
|
|
86
|
+
// Evaluate every removal into a local — never short-circuit.
|
|
87
87
|
let changed = false;
|
|
88
88
|
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
89
89
|
const removed = removeHooks(settings, hookType, isMemoryHook(marker));
|