devflow-kit 3.0.1 → 3.2.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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- 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 +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- 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 +183 -59
- 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/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- 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 +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -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 +13 -8
- 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/queue-drain.js +31 -0
- 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 +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.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 +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- 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 +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- 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 +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -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/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- 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/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rendering for the post-install summary — pure functions that turn an
|
|
3
|
-
* {@link InstallReport} into lines, logging nothing
|
|
3
|
+
* {@link InstallReport} into lines, logging nothing.
|
|
4
4
|
*
|
|
5
5
|
* Its own module rather than a section of init.ts because `devflow tracker --set`
|
|
6
6
|
* renders the same overlay outcomes from a different command. A CLI command
|
|
@@ -18,18 +18,18 @@ import { prefixSkillName } from '../../core/plugins.js';
|
|
|
18
18
|
* shadowed, and a unit it could not refresh is left in one of the states
|
|
19
19
|
* {@link OverlayFailureState} enumerates — running on the previous install, half
|
|
20
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 —
|
|
21
|
+
* the filesystem at a glance, so all of it reaches the summary — a report field
|
|
22
22
|
* with no render site is not a report, and a render site that flattens four states into
|
|
23
23
|
* one sentence is the same defect one layer up.
|
|
24
24
|
*
|
|
25
|
-
* Pure function — returns lines, logs nothing
|
|
25
|
+
* Pure function — returns lines, logs nothing.
|
|
26
26
|
*
|
|
27
27
|
* @param skillName - Bare name of the skill hosting the generated references,
|
|
28
28
|
* rendered `devflow:`-prefixed. Defaults to the core constant the build path and
|
|
29
29
|
* the installer's overlay trigger both read, so the renderer is never a third
|
|
30
|
-
* independent statement of which skill owns them —
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* independent statement of which skill owns them — a divergence where changing
|
|
31
|
+
* the answer means finding every retyped spelling and nothing fails if one is
|
|
32
|
+
* missed.
|
|
33
33
|
*/
|
|
34
34
|
export function formatOverlaySummary(report, skillName = SKILL_REFS_SKILL_NAME) {
|
|
35
35
|
const lines = [];
|
|
@@ -59,7 +59,7 @@ export function formatOverlaySummary(report, skillName = SKILL_REFS_SKILL_NAME)
|
|
|
59
59
|
* be told about.
|
|
60
60
|
*
|
|
61
61
|
* Exported because `devflow tracker --set` renders the same states when it aborts
|
|
62
|
-
* on an overlay failure (
|
|
62
|
+
* on an overlay failure (one sentence per state, in one place,
|
|
63
63
|
* rather than a second wording that drifts).
|
|
64
64
|
*
|
|
65
65
|
* Exhaustive over {@link OverlayFailureState} — a new state added to the union without a
|
|
@@ -103,7 +103,7 @@ export function describeOverlayFailureState(state) {
|
|
|
103
103
|
* The delta line is emitted only when something moved: on a steady-state re-init
|
|
104
104
|
* the counts are noise.
|
|
105
105
|
*
|
|
106
|
-
* Pure function — returns lines, logs nothing
|
|
106
|
+
* Pure function — returns lines, logs nothing.
|
|
107
107
|
*
|
|
108
108
|
* @param previous - The provider recorded by the PRIOR manifest, or undefined on
|
|
109
109
|
* a first install. Rendered only when it differs from `provider`.
|
|
@@ -144,7 +144,7 @@ export function formatTrackerAssetSummary(input) {
|
|
|
144
144
|
* how the selection was assembled, and a duplicate name in either list is a
|
|
145
145
|
* manifest detail rather than a different selection.
|
|
146
146
|
*
|
|
147
|
-
* Pure function
|
|
147
|
+
* Pure function.
|
|
148
148
|
*
|
|
149
149
|
* @param previousPlugins - `manifest.plugins` as it stands before this run, or
|
|
150
150
|
* `null` when there is no prior manifest.
|
|
@@ -176,7 +176,7 @@ export function isPluginListUnchanged(previousPlugins, effectivePluginNames) {
|
|
|
176
176
|
* it is FALSE when there is no prior manifest: a first install removed nothing a
|
|
177
177
|
* user had, so there is no upgrade to explain (design review L2).
|
|
178
178
|
*
|
|
179
|
-
* Pure function — returns lines, logs nothing
|
|
179
|
+
* Pure function — returns lines, logs nothing.
|
|
180
180
|
*/
|
|
181
181
|
export function formatSkillScopeSummary(report, pluginListUnchanged) {
|
|
182
182
|
const lines = [];
|
|
@@ -3,25 +3,119 @@ 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 { firstSymbolicLink } from '../../core/linked-path.js';
|
|
13
|
+
import { formatRefusedDrain } from '../../core/queue-drain.js';
|
|
14
|
+
import { formatLearningStoreUnavailable, loadLearningStore, } from '../../core/learning-store.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Shared helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** What the read and restore commands say about a project with no `.devflow/learning/`. */
|
|
19
|
+
const NO_LEARNING_DATA = 'No learning data in this project yet.';
|
|
20
|
+
/**
|
|
21
|
+
* How long a writer here (--restore, --clear, --reset) waits for the learning lock
|
|
22
|
+
* before it refuses as busy. An op holds the lock for milliseconds, so a longer
|
|
23
|
+
* wait means a stuck holder, and the store's own 30 s wait would leave the command
|
|
24
|
+
* looking hung.
|
|
25
|
+
*/
|
|
26
|
+
const LOCK_WAIT_MS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* Code point ranges JSON.stringify leaves raw that a terminal may act on, or may
|
|
29
|
+
* display reordered: DEL and the C1 controls, the directional marks, the line and
|
|
30
|
+
* paragraph separators, and the bidirectional embeddings, overrides and isolates.
|
|
31
|
+
*/
|
|
32
|
+
const TERMINAL_UNSAFE_RANGES = [
|
|
33
|
+
[0x7f, 0x9f],
|
|
34
|
+
[0x200e, 0x200f],
|
|
35
|
+
[0x2028, 0x2029],
|
|
36
|
+
[0x202a, 0x202e],
|
|
37
|
+
[0x2066, 0x2069],
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* `value` as pretty JSON a terminal shows as written: every character in
|
|
41
|
+
* TERMINAL_UNSAFE_RANGES becomes its `\uXXXX` escape. Such characters can occur
|
|
42
|
+
* only inside JSON strings, where the escape means the same character, so the
|
|
43
|
+
* text still parses back to `value`.
|
|
44
|
+
*/
|
|
45
|
+
function terminalSafeJson(value) {
|
|
46
|
+
let text = '';
|
|
47
|
+
for (const ch of JSON.stringify(value, null, 2)) {
|
|
48
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
49
|
+
const unsafe = TERMINAL_UNSAFE_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
50
|
+
text += unsafe ? `\\u${code.toString(16).padStart(4, '0')}` : ch;
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
/** `1 decision`, `2 decisions`. */
|
|
55
|
+
function counted(count, noun) {
|
|
56
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The learning store (D-LEARNING-STORE-SEAM), or null after reporting why it
|
|
60
|
+
* cannot be used and setting exit code 1.
|
|
61
|
+
*/
|
|
62
|
+
function requireStore() {
|
|
63
|
+
const loaded = loadLearningStore();
|
|
64
|
+
if (loaded.ok)
|
|
65
|
+
return loaded.value;
|
|
66
|
+
p.log.error(`Learning: ${formatLearningStoreUnavailable(loaded.error)}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** What a writer says when the store refused; `undone` completes "Nothing was …". */
|
|
71
|
+
function storeRefusal(error, undone) {
|
|
72
|
+
switch (error.kind) {
|
|
73
|
+
case 'no-learning-dir': return NO_LEARNING_DATA;
|
|
74
|
+
case 'busy': return `The learning store is busy: another run holds its lock. Nothing was ${undone}; try again in a moment.`;
|
|
75
|
+
default: return error.message;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Ask `message` on a terminal and say whether to go on, logging `cancelled` when the
|
|
80
|
+
* answer is no. With no terminal there is no one to ask: a script that passed the
|
|
81
|
+
* flag has decided.
|
|
82
|
+
*/
|
|
83
|
+
async function confirmOnTerminal(message, cancelled) {
|
|
84
|
+
if (!process.stdin.isTTY)
|
|
85
|
+
return true;
|
|
86
|
+
const confirmed = await p.confirm({ message, initialValue: false });
|
|
87
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
88
|
+
p.log.info(cancelled);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/** True when `dir` is a directory; false when nothing, or something else, is there. */
|
|
94
|
+
async function isDirectory(dir) {
|
|
95
|
+
try {
|
|
96
|
+
return (await fs.stat(dir)).isDirectory();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const code = err.code;
|
|
100
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
101
|
+
return false;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
13
105
|
// ---------------------------------------------------------------------------
|
|
14
106
|
// Sub-command handlers
|
|
15
107
|
// ---------------------------------------------------------------------------
|
|
16
108
|
function printUsage() {
|
|
17
109
|
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 --
|
|
110
|
+
p.note(`${color.cyan('devflow learning --enable')} Enable learning in every project (a repository can opt out)\n` +
|
|
111
|
+
`${color.cyan('devflow learning --disable')} Disable learning in every project (drains this project's queue)\n` +
|
|
112
|
+
`${color.cyan('devflow learning --status')} Show learning status and entry counts\n` +
|
|
113
|
+
`${color.cyan('devflow learning --list')} List entries, inactive entries and observations\n` +
|
|
114
|
+
`${color.cyan('devflow learning --show <id>')} Print one entry or observation as JSON\n` +
|
|
115
|
+
`${color.cyan('devflow learning --restore <id>')} Make an inactive entry active again\n` +
|
|
116
|
+
`${color.cyan('devflow learning --configure')} Configuration wizard\n` +
|
|
117
|
+
`${color.cyan('devflow learning --clear')} Drop the observations no entry uses, and drain the queue\n` +
|
|
118
|
+
`${color.cyan('devflow learning --reset')} Remove all learning state files`, 'Usage');
|
|
25
119
|
p.outro(color.dim('Detects architectural decisions and known pitfalls from your sessions'));
|
|
26
120
|
}
|
|
27
121
|
/**
|
|
@@ -40,10 +134,35 @@ async function requireLedgerRoot(actionSuffix) {
|
|
|
40
134
|
}
|
|
41
135
|
return ledgerRoot;
|
|
42
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The --status entry lines: active entries by type, inactive ones by status (in
|
|
139
|
+
* the store's order, each status present), the active entries still in the v1
|
|
140
|
+
* format — the migration still to do — and the observations.
|
|
141
|
+
*/
|
|
142
|
+
function entryCountLines(listing, logRowCount, inactiveStatuses) {
|
|
143
|
+
const active = listing.active;
|
|
144
|
+
const decisions = active.filter(entry => entry.type === 'decision').length;
|
|
145
|
+
const pitfalls = active.filter(entry => entry.type === 'pitfall').length;
|
|
146
|
+
const byStatus = inactiveStatuses
|
|
147
|
+
.map(status => ({ status, count: listing.inactive.filter(entry => entry.status === status).length }))
|
|
148
|
+
.filter(({ count }) => count > 0)
|
|
149
|
+
.map(({ status, count }) => `${status} ${count}`);
|
|
150
|
+
const legacy = active.filter(entry => entry.schema === 1).length;
|
|
151
|
+
return [
|
|
152
|
+
`Entries: ${active.length} active (${counted(decisions, 'decision')}, ${counted(pitfalls, 'pitfall')}), `
|
|
153
|
+
+ `${listing.inactive.length} inactive${byStatus.length > 0 ? ` (${byStatus.join(', ')})` : ''}`,
|
|
154
|
+
`Legacy v1 entries: ${legacy} of ${active.length} active`,
|
|
155
|
+
`Observations: ${logRowCount} in the log, ${listing.observations.length} not yet promoted`,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* `--status`: the machine switch, then the counts the store reads. Reads only:
|
|
160
|
+
* malformed lines are counted, never quarantined, and no scope is checked.
|
|
161
|
+
*/
|
|
43
162
|
async function handleStatus() {
|
|
44
163
|
// D-FEATURES-NARROW-ONLY: the machine switch is the manifest's and reads the
|
|
45
164
|
// same from every directory; a repository layer can only narrow it, and adds a
|
|
46
|
-
// line only when it does. The
|
|
165
|
+
// line only when it does. The entry counts are per-project.
|
|
47
166
|
const enabled = await readMachineFeature(getDevFlowDirectory(), 'learning');
|
|
48
167
|
const settingsModule = loadSettingsModule();
|
|
49
168
|
const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'learning') : null;
|
|
@@ -54,69 +173,106 @@ async function handleStatus() {
|
|
|
54
173
|
p.log.warn(trackedWarning);
|
|
55
174
|
const ledgerRoot = await getLedgerRoot();
|
|
56
175
|
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');
|
|
176
|
+
p.log.info(`${stateLine}\nEntries: not in a git project`);
|
|
177
|
+
return;
|
|
72
178
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
179
|
+
const loaded = loadLearningStore();
|
|
180
|
+
if (!loaded.ok) {
|
|
181
|
+
p.log.info(`${stateLine}\nEntries: unavailable (${formatLearningStoreUnavailable(loaded.error)})`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const store = loaded.value;
|
|
185
|
+
const { ledgerRows, logRows, rejected } = store.readLearningState(ledgerRoot);
|
|
186
|
+
const listing = store.buildListing(ledgerRows, logRows, { rejected });
|
|
187
|
+
p.log.info([stateLine, ...entryCountLines(listing, logRows.length, store.INACTIVE_STATUSES)].join('\n'));
|
|
188
|
+
const { ledger, log } = listing.malformed;
|
|
189
|
+
if (ledger + log > 0) {
|
|
190
|
+
p.log.warn(`Malformed lines skipped: ${ledger} in the ledger, ${log} in the log. ` +
|
|
191
|
+
'The next op that rewrites a file moves its malformed lines to a .rejected.jsonl file beside it.');
|
|
77
192
|
}
|
|
78
|
-
p.log.info(lines.join('\n'));
|
|
79
|
-
warnIfInvalid(invalidCount);
|
|
80
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* `--list`: the store's listing — the same text json-helper's `list` op prints —
|
|
196
|
+
* on stdout. Read-only. Outside a git project it reads the current directory.
|
|
197
|
+
*/
|
|
81
198
|
async function handleList() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
199
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
200
|
+
const store = requireStore();
|
|
201
|
+
if (!store)
|
|
202
|
+
return;
|
|
203
|
+
const listed = store.readListing(root);
|
|
204
|
+
if (!listed.ok) {
|
|
205
|
+
if (listed.error.kind === 'no-learning-dir') {
|
|
206
|
+
p.log.info(NO_LEARNING_DATA);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
p.log.error(listed.error.message);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
return;
|
|
91
212
|
}
|
|
92
|
-
|
|
93
|
-
|
|
213
|
+
process.stdout.write(`${store.formatListing(listed.value)}\n`);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* `--show <id>`: one entry, by its anchor or its observation id, as the JSON
|
|
217
|
+
* json-helper's `show` op prints, made terminal-safe. Read-only. Outside a git
|
|
218
|
+
* project it reads the current directory.
|
|
219
|
+
*/
|
|
220
|
+
async function handleShow(key) {
|
|
221
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
222
|
+
const store = requireStore();
|
|
223
|
+
if (!store)
|
|
224
|
+
return;
|
|
225
|
+
const shown = store.showByKey(root, key);
|
|
226
|
+
if (!shown.ok) {
|
|
227
|
+
p.log.error(shown.error.kind === 'no-learning-dir' ? NO_LEARNING_DATA : shown.error.message);
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
94
230
|
}
|
|
95
|
-
|
|
96
|
-
|
|
231
|
+
process.stdout.write(`${terminalSafeJson(shown.value)}\n`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* `--restore <id>`: make an inactive entry active again through the store's
|
|
235
|
+
* restoreAnchor, which clears its notes and its verification so maintenance
|
|
236
|
+
* reviews it again, and re-renders the files. A refusal writes nothing.
|
|
237
|
+
*/
|
|
238
|
+
async function handleRestore(anchor) {
|
|
239
|
+
const ledgerRoot = await requireLedgerRoot('restore not performed');
|
|
240
|
+
if (!ledgerRoot) {
|
|
241
|
+
process.exitCode = 1;
|
|
97
242
|
return;
|
|
98
243
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
244
|
+
const store = requireStore();
|
|
245
|
+
if (!store)
|
|
246
|
+
return;
|
|
247
|
+
if (!store.ANCHOR_ID_RE.test(anchor)) {
|
|
248
|
+
p.log.error(`--restore takes an entry id (ADR-NNN or PF-NNN), not ${JSON.stringify(anchor)}`);
|
|
249
|
+
process.exitCode = 1;
|
|
103
250
|
return;
|
|
104
251
|
}
|
|
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})`);
|
|
252
|
+
const restored = store.restoreAnchor(ledgerRoot, anchor, { timeoutMs: LOCK_WAIT_MS });
|
|
253
|
+
if (!restored.ok) {
|
|
254
|
+
p.log.error(storeRefusal(restored.error, 'restored'));
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
116
257
|
}
|
|
117
|
-
|
|
118
|
-
p.outro(color.dim(`${filtered.length} observation(s) total`));
|
|
258
|
+
p.log.success(`Restored ${restored.value.anchor_id} (${restored.value.status}); it is due for review again.`);
|
|
119
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* The scope choices of `devflow learning --configure`.
|
|
262
|
+
*
|
|
263
|
+
* D-LEARNING-MODEL-PRECEDENCE: the session-start hook takes the first layer that
|
|
264
|
+
* supplies a model — the project file, then a `devflow agents` Learning mapping,
|
|
265
|
+
* then the global file. A global file therefore has no effect for a user who has
|
|
266
|
+
* such a mapping, and its hint says so.
|
|
267
|
+
*/
|
|
268
|
+
export const CONFIGURE_SCOPE_OPTIONS = [
|
|
269
|
+
{ value: 'project', label: 'Project', hint: 'This project only (.devflow/learning/learning.json)' },
|
|
270
|
+
{
|
|
271
|
+
value: 'global',
|
|
272
|
+
label: 'Global',
|
|
273
|
+
hint: 'All projects (~/.devflow/learning.json); a devflow agents Learning mapping takes precedence over it',
|
|
274
|
+
},
|
|
275
|
+
];
|
|
120
276
|
async function handleConfigure() {
|
|
121
277
|
p.intro(color.bgCyan(color.black(' Learning Configuration ')));
|
|
122
278
|
const model = await p.select({
|
|
@@ -141,10 +297,7 @@ async function handleConfigure() {
|
|
|
141
297
|
}
|
|
142
298
|
const scope = await p.select({
|
|
143
299
|
message: 'Configuration scope',
|
|
144
|
-
options: [
|
|
145
|
-
{ value: 'project', label: 'Project', hint: 'This project only (.devflow/learning/learning.json)' },
|
|
146
|
-
{ value: 'global', label: 'Global', hint: 'All projects (~/.devflow/learning.json)' },
|
|
147
|
-
],
|
|
300
|
+
options: [...CONFIGURE_SCOPE_OPTIONS],
|
|
148
301
|
});
|
|
149
302
|
if (p.isCancel(scope)) {
|
|
150
303
|
p.cancel('Configuration cancelled.');
|
|
@@ -166,91 +319,92 @@ async function handleConfigure() {
|
|
|
166
319
|
// from the ledger ($LEDGER_ROOT/.devflow/learning/), so write it there — the
|
|
167
320
|
// main checkout in a linked worktree; the current directory outside git.
|
|
168
321
|
const projectRoot = (await getLedgerRoot()) ?? process.cwd();
|
|
169
|
-
|
|
322
|
+
const learningDir = getLearningDir(projectRoot);
|
|
170
323
|
const projectConfigPath = getLearningTuningConfigPath(projectRoot);
|
|
324
|
+
// D-CLI-NO-SYMLINK (core/linked-path.ts): nothing is written through a .devflow,
|
|
325
|
+
// a learning folder or a learning.json that is a symbolic link.
|
|
326
|
+
const linked = await firstSymbolicLink([path.dirname(learningDir), learningDir, projectConfigPath]);
|
|
327
|
+
if (linked !== null) {
|
|
328
|
+
p.log.error(`Project config not written: ${linked} is a symbolic link, and devflow writes nothing through one`);
|
|
329
|
+
process.exitCode = 1;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await fs.mkdir(learningDir, { recursive: true });
|
|
171
333
|
await fs.writeFile(projectConfigPath, configJson, 'utf-8');
|
|
172
334
|
p.log.success(`Project config written to ${color.dim(projectConfigPath)}`);
|
|
173
335
|
}
|
|
174
336
|
p.outro(color.green('Configuration saved.'));
|
|
175
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* `--reset`: remove all learning state — entries, observations, rendered files,
|
|
340
|
+
* the tuning config, and the queue with its claim — through the store's
|
|
341
|
+
* resetLearning, under the learning lock every learning writer takes
|
|
342
|
+
* (D-ONE-LEARNING-LOCK, D-RESET-UNDER-LOCK). A project with no learning directory
|
|
343
|
+
* has nothing to reset, and nothing is created there (D-NO-STRAY-TREE).
|
|
344
|
+
*/
|
|
176
345
|
async function handleReset() {
|
|
177
346
|
const ledgerRoot = await requireLedgerRoot('reset not performed');
|
|
178
347
|
if (!ledgerRoot)
|
|
179
348
|
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.');
|
|
349
|
+
const store = requireStore();
|
|
350
|
+
if (!store)
|
|
351
|
+
return;
|
|
352
|
+
if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
|
|
353
|
+
p.log.info('No learning data to reset.');
|
|
191
354
|
return;
|
|
192
355
|
}
|
|
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);
|
|
356
|
+
const proceed = await confirmOnTerminal('Remove all learning state files? This cannot be undone.', 'Reset cancelled.');
|
|
357
|
+
if (!proceed)
|
|
358
|
+
return;
|
|
359
|
+
const reset = store.resetLearning(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
360
|
+
if (!reset.ok) {
|
|
361
|
+
if (reset.error.kind === 'no-learning-dir') {
|
|
362
|
+
p.log.info('No learning data to reset.');
|
|
363
|
+
return;
|
|
221
364
|
}
|
|
222
|
-
|
|
365
|
+
p.log.error(storeRefusal(reset.error, 'reset'));
|
|
366
|
+
process.exitCode = 1;
|
|
367
|
+
return;
|
|
223
368
|
}
|
|
369
|
+
p.log.success('Reset complete — removed .devflow/learning/ state.');
|
|
224
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* `--clear`: drop the observations no entry uses (D-CLEAR-UNREFERENCED), then
|
|
373
|
+
* drain the learning queue. The queue is drained only once the clear succeeded:
|
|
374
|
+
* a busy lock or a refusal leaves both the log and the queue as they were, so a
|
|
375
|
+
* failed clear changes nothing.
|
|
376
|
+
*/
|
|
225
377
|
async function handleClear() {
|
|
226
378
|
const ledgerRoot = await requireLedgerRoot('clear not performed');
|
|
227
379
|
if (!ledgerRoot)
|
|
228
380
|
return;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
p.log.info('No decisions log to clear.');
|
|
381
|
+
const store = requireStore();
|
|
382
|
+
if (!store)
|
|
383
|
+
return;
|
|
384
|
+
if (!(await isDirectory(getLearningDir(ledgerRoot)))) {
|
|
385
|
+
p.log.info('No learning data to clear.');
|
|
235
386
|
return;
|
|
236
387
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
p.log.info('
|
|
388
|
+
const proceed = await confirmOnTerminal('Drop every observation no entry uses? Entries and the observations they use are kept. This cannot be undone.', 'Clear cancelled.');
|
|
389
|
+
if (!proceed)
|
|
390
|
+
return;
|
|
391
|
+
const cleared = store.clearUnreferenced(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
392
|
+
if (!cleared.ok) {
|
|
393
|
+
if (cleared.error.kind === 'no-learning-dir') {
|
|
394
|
+
p.log.info('No learning data to clear.');
|
|
244
395
|
return;
|
|
245
396
|
}
|
|
397
|
+
p.log.error(storeRefusal(cleared.error, 'cleared'));
|
|
398
|
+
process.exitCode = 1;
|
|
399
|
+
return;
|
|
246
400
|
}
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
401
|
+
// A mid-run Learning agent whose claimed batch vanishes stops without further
|
|
402
|
+
// writes — the desired outcome of clearing.
|
|
403
|
+
const drain = await drainLearningQueue(ledgerRoot);
|
|
404
|
+
p.log.success(`Cleared ${counted(cleared.value.cleared, 'observation')} no entry uses and kept ${cleared.value.kept} ` +
|
|
405
|
+
`that entries use${drain.drained ? '; drained the learning queue.' : '.'}`);
|
|
406
|
+
if (!drain.drained)
|
|
407
|
+
p.log.warn(formatRefusedDrain('learning', drain.linkedFolder));
|
|
254
408
|
}
|
|
255
409
|
/**
|
|
256
410
|
* `--enable` / `--disable`: the machine-wide switch (D-FEATURES-NARROW-ONLY),
|
|
@@ -275,7 +429,9 @@ async function handleToggle(enabled) {
|
|
|
275
429
|
// batch vanishes aborts without changes — the desired outcome of disabling.
|
|
276
430
|
const ledgerRoot = await getLedgerRoot();
|
|
277
431
|
if (ledgerRoot) {
|
|
278
|
-
await drainLearningQueue(ledgerRoot);
|
|
432
|
+
const drain = await drainLearningQueue(ledgerRoot);
|
|
433
|
+
if (!drain.drained)
|
|
434
|
+
p.log.warn(formatRefusedDrain('learning', drain.linkedFolder));
|
|
279
435
|
}
|
|
280
436
|
p.log.success('Learning disabled in every project');
|
|
281
437
|
}
|
|
@@ -283,24 +439,26 @@ export const learningCommand = new Command('learning')
|
|
|
283
439
|
.description('Enable or disable learning (decision/pitfall detection) in every project')
|
|
284
440
|
.option('--enable', 'Enable learning in every project (a repository can opt out)')
|
|
285
441
|
.option('--disable', 'Disable learning in every project')
|
|
286
|
-
.option('--status', 'Show learning status and
|
|
287
|
-
.option('--list', '
|
|
442
|
+
.option('--status', 'Show learning status and entry counts')
|
|
443
|
+
.option('--list', 'List entries, inactive entries with their notes, and observations')
|
|
444
|
+
.option('--show <id>', 'Print one entry (ADR-NNN or PF-NNN) or observation (obs_...) as JSON')
|
|
445
|
+
.option('--restore <id>', 'Make an inactive entry active again')
|
|
288
446
|
.option('--configure', 'Interactive configuration wizard for learning.json')
|
|
289
|
-
.option('--clear', '
|
|
447
|
+
.option('--clear', 'Drop the observations no entry uses, and drain the learning queue')
|
|
290
448
|
.option('--reset', 'Remove all learning state files and artifacts')
|
|
291
449
|
.action(async (options) => {
|
|
292
450
|
const knownFlags = [
|
|
293
|
-
'enable', 'disable', 'status', 'list', 'configure',
|
|
451
|
+
'enable', 'disable', 'status', 'list', 'show', 'restore', 'configure',
|
|
294
452
|
'clear', 'reset',
|
|
295
453
|
];
|
|
296
|
-
const hasFlag = knownFlags.some((f) => options[f]);
|
|
454
|
+
const hasFlag = knownFlags.some((f) => options[f] !== undefined);
|
|
297
455
|
if (!hasFlag) {
|
|
298
456
|
printUsage();
|
|
299
457
|
return;
|
|
300
458
|
}
|
|
301
|
-
// Thin router —
|
|
302
|
-
//
|
|
303
|
-
//
|
|
459
|
+
// Thin router — the read-only commands first (status, list, show), then
|
|
460
|
+
// configure, reset, clear, restore, enable, disable. Each handler owns its own
|
|
461
|
+
// path resolution and I/O.
|
|
304
462
|
if (options.status) {
|
|
305
463
|
await handleStatus();
|
|
306
464
|
return;
|
|
@@ -309,6 +467,10 @@ export const learningCommand = new Command('learning')
|
|
|
309
467
|
await handleList();
|
|
310
468
|
return;
|
|
311
469
|
}
|
|
470
|
+
if (options.show !== undefined) {
|
|
471
|
+
await handleShow(options.show);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
312
474
|
if (options.configure) {
|
|
313
475
|
await handleConfigure();
|
|
314
476
|
return;
|
|
@@ -321,6 +483,10 @@ export const learningCommand = new Command('learning')
|
|
|
321
483
|
await handleClear();
|
|
322
484
|
return;
|
|
323
485
|
}
|
|
486
|
+
if (options.restore !== undefined) {
|
|
487
|
+
await handleRestore(options.restore);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
324
490
|
if (options.enable) {
|
|
325
491
|
await handleToggle(true);
|
|
326
492
|
return;
|