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.
- package/CHANGELOG.md +479 -1
- package/README.md +15 -15
- package/dist/agents/code.md +6 -8
- package/dist/agents/design.md +1 -1
- package/dist/agents/fix.md +7 -8
- package/dist/agents/git.md +3 -3
- package/dist/agents/harden.md +1 -1
- package/dist/agents/refactor.md +2 -4
- package/dist/agents/review.md +4 -4
- package/dist/agents/skim.md +2 -2
- package/dist/agents/triage.md +9 -2
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/curation-move-io.js +102 -0
- package/dist/cli/commands/curation-move-step.js +374 -0
- package/dist/cli/commands/curation.js +808 -0
- package/dist/cli/commands/gate-level-prompts.js +428 -0
- package/dist/cli/commands/gates.js +35 -11
- package/dist/cli/commands/init-seed.js +11 -8
- package/dist/cli/commands/init.js +244 -57
- package/dist/cli/commands/memory.js +60 -13
- package/dist/cli/commands/prompt-io.js +37 -0
- package/dist/cli/commands/uninstall.js +157 -17
- package/dist/cli.js +2 -2
- package/dist/commands/bug-analysis.md +25 -36
- package/dist/commands/code-review.md +40 -44
- package/dist/commands/debug.md +5 -5
- package/dist/commands/dynamic-build.md +26 -25
- package/dist/commands/dynamic-plan.md +5 -5
- package/dist/commands/dynamic-profile.md +5 -5
- package/dist/commands/dynamic-tickets.md +5 -5
- package/dist/commands/explore.md +6 -6
- package/dist/commands/implement.md +53 -82
- package/dist/commands/plan.md +6 -6
- package/dist/commands/release.md +5 -5
- package/dist/commands/research.md +6 -6
- package/dist/commands/resolve.md +39 -41
- package/dist/commands/self-review.md +5 -13
- package/dist/core/area-pause-notice.js +44 -0
- package/dist/core/assets.js +20 -20
- package/dist/core/curate-tuning-config.js +5 -6
- package/dist/core/curation-move-notice.js +84 -0
- package/dist/core/curation-move-skip.js +35 -0
- package/dist/core/curation-move.js +35 -0
- package/dist/core/curation-queue-cleanup.js +135 -0
- package/dist/core/curation-store.js +99 -0
- package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
- package/dist/core/echo-value.js +11 -5
- package/dist/core/evidence-policy.js +11 -9
- package/dist/core/feature-switch.js +95 -19
- package/dist/core/fs-atomic.js +1 -1
- package/dist/core/gate-level.js +10 -2
- package/dist/core/git.js +16 -14
- package/dist/core/ledger-root.js +82 -22
- package/dist/core/linked-path.js +4 -4
- package/dist/core/manifest-lock.js +238 -0
- package/dist/core/manifest.js +58 -14
- package/dist/core/migrations.js +1 -13
- package/dist/core/observations.js +2 -2
- package/dist/core/plugins.js +19 -19
- package/dist/core/pool.js +26 -0
- package/dist/core/project-paths.js +103 -34
- package/dist/core/queue-drain.js +2 -2
- package/dist/core/tracked-areas.js +159 -0
- package/dist/core/tracker.js +1 -1
- package/dist/{learning-off → curation-off}/agents/code.md +6 -8
- package/dist/{learning-off → curation-off}/agents/fix.md +7 -8
- package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
- package/dist/{learning-off → curation-off}/agents/review.md +3 -3
- package/dist/{learning-off → curation-off}/agents/triage.md +8 -1
- package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
- package/dist/{learning-off → curation-off}/commands/code-review.md +37 -41
- package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
- package/dist/{learning-off → curation-off}/commands/dynamic-build.md +23 -22
- package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
- package/dist/{learning-off → curation-off}/commands/implement.md +50 -79
- package/dist/{learning-off → curation-off}/commands/plan.md +2 -2
- package/dist/{learning-off → curation-off}/commands/resolve.md +36 -38
- package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
- package/dist/hud/components/{learning-counts.js → curation-counts.js} +63 -17
- package/dist/hud/config.js +1 -1
- package/dist/hud/index.js +6 -6
- package/dist/hud/render.js +3 -3
- package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
- package/dist/skills/git/references/pr/post-review-summary.md +12 -11
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
- package/dist/skills/git/references/pr/validate-branch.md +8 -5
- package/dist/skills/git/references/publication-gate.md +6 -7
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
- package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
- package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
- package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
- package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
- package/dist/targets/claude-code/installer.js +15 -15
- package/dist/targets/claude-code/language-stamp.js +5 -5
- package/dist/targets/claude-code/post-install.js +2 -2
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +3 -2
- package/src/assets/agents/_partials/_code.mds +11 -11
- package/src/assets/agents/code.mds +6 -6
- package/src/assets/agents/curate.md +8 -8
- package/src/assets/agents/debug.mds +6 -6
- package/src/assets/agents/design.mds +18 -18
- package/src/assets/agents/evaluate.md +6 -12
- package/src/assets/agents/fix.mds +7 -6
- package/src/assets/agents/git.mds +3 -3
- package/src/assets/agents/harden.mds +7 -7
- package/src/assets/agents/knowledge.mds +23 -23
- package/src/assets/agents/refactor.mds +2 -2
- package/src/assets/agents/research.mds +15 -15
- package/src/assets/agents/review.mds +43 -43
- package/src/assets/agents/skim.mds +11 -11
- package/src/assets/agents/triage.mds +29 -22
- package/src/assets/agents/validate.md +30 -50
- package/src/assets/commands/_partials/_decisions.mds +22 -20
- package/src/assets/commands/_partials/_engine.mds +8 -8
- package/src/assets/commands/_partials/_knowledge.mds +29 -15
- package/src/assets/commands/_partials/_preamble.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +3 -3
- package/src/assets/commands/_partials/_wave.mds +7 -7
- package/src/assets/commands/bug-analysis.mds +37 -48
- package/src/assets/commands/code-review.mds +53 -57
- package/src/assets/commands/debug.mds +14 -14
- package/src/assets/commands/dynamic-build.mds +38 -37
- package/src/assets/commands/dynamic-plan.mds +22 -22
- package/src/assets/commands/dynamic-profile.mds +2 -2
- package/src/assets/commands/dynamic-tickets.mds +8 -8
- package/src/assets/commands/explore.mds +20 -20
- package/src/assets/commands/implement.mds +77 -98
- package/src/assets/commands/plan.mds +27 -27
- package/src/assets/commands/release.mds +12 -12
- package/src/assets/commands/research.mds +19 -19
- package/src/assets/commands/resolve.mds +66 -60
- package/src/assets/commands/self-review.mds +20 -20
- package/src/assets/mds/git/_ops.mds +9 -2
- package/src/assets/mds/git/_ops_pr.mds +21 -11
- package/src/assets/mds/git/_ops_steps.mds +20 -4
- package/src/assets/mds/git/_pr.mds +21 -18
- package/src/assets/mds/git/_references.mds +6 -7
- package/src/assets/mds/tracker/_github.mds +1 -4
- package/src/assets/mds/tracker/_jira.mds +1 -4
- package/src/assets/mds/tracker/_linear.mds +1 -4
- package/src/assets/rules/quality.md +1 -0
- package/src/assets/scripts/ci-wait.cjs +389 -92
- package/src/assets/scripts/hooks/background-memory-update +81 -3
- package/src/assets/scripts/hooks/capture-prompt +12 -12
- package/src/assets/scripts/hooks/capture-question +11 -11
- package/src/assets/scripts/hooks/capture-turn +12 -12
- package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
- package/src/assets/scripts/hooks/curation-move +293 -0
- package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
- package/src/assets/scripts/hooks/ensure-proxy +4 -4
- package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
- package/src/assets/scripts/hooks/git-marker +81 -0
- package/src/assets/scripts/hooks/json-helper.cjs +176 -51
- package/src/assets/scripts/hooks/json-parse +1 -1
- package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
- package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +308 -122
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
- package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +42 -22
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +452 -0
- package/src/assets/scripts/hooks/memory-permission +10 -2
- package/src/assets/scripts/hooks/memory-worker +12 -1
- package/src/assets/scripts/hooks/pre-compact-memory +17 -16
- package/src/assets/scripts/hooks/queue-append +444 -102
- package/src/assets/scripts/hooks/resolve-project-root +2 -2
- package/src/assets/scripts/hooks/session-start-context +198 -72
- package/src/assets/scripts/hooks/session-start-memory +42 -43
- package/src/assets/scripts/lib/project-config.cjs +12 -5
- package/src/assets/scripts/pr-evidence.cjs +5 -2
- package/src/assets/scripts/resolve-base.cjs +1043 -0
- package/src/assets/scripts/resolve-settings.cjs +283 -45
- package/src/assets/scripts/verify-evidence.cjs +307 -75
- package/src/assets/skills/agent-references/SKILL.md +11 -2
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +29 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +25 -0
- package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
- package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
- package/src/assets/skills/apply-decisions/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +7 -7
- package/src/assets/skills/docs-framework/references/patterns.md +3 -4
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
- package/templates/lint-pack/.lint-pack.conf +86 -0
- package/templates/lint-pack/README.md +190 -0
- package/templates/lint-pack/lint-pack.sh +905 -0
- package/dist/cli/commands/learning.js +0 -554
- package/dist/core/learning-queue-cleanup.js +0 -27
- package/dist/core/learning-store.js +0 -61
- /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
import color from 'picocolors';
|
|
7
|
+
import { CURATION_TUNING_FILE_NAME, LEGACY_NAMES, getCurationDir, getGlobalCurateTuningConfigPath, } from '../../core/project-paths.js';
|
|
8
|
+
import { loadShippedAgentDefaults } from '../../core/agent-models.js';
|
|
9
|
+
import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
|
|
10
|
+
import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
|
|
11
|
+
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
12
|
+
import { applyCurationToggle, describeCurationConverge, installedScriptsSkew, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
|
|
13
|
+
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
14
|
+
import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
|
|
15
|
+
import { probeTrackedArea } from '../../core/tracked-areas.js';
|
|
16
|
+
import { drainCurationQueue, drainDisabledCurationQueue, formatCurationNotDrained } from '../../core/curation-queue-cleanup.js';
|
|
17
|
+
import { firstSymbolicLink } from '../../core/linked-path.js';
|
|
18
|
+
import { describeMoveText, describeOutcome } from './curation-move-step.js';
|
|
19
|
+
import { describeMoveNotice, gatherMoveNotice } from '../../core/curation-move-notice.js';
|
|
20
|
+
import { isMoveSkipReason } from '../../core/curation-move-skip.js';
|
|
21
|
+
import { runSharedMove } from './curation-move-io.js';
|
|
22
|
+
import { formatCurationStoreUnavailable, loadCurationStore, } from '../../core/curation-store.js';
|
|
23
|
+
import { loadCurationMove } from '../../core/curation-move.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shared helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** What the read and restore commands say about a project with no `.devflow/curation/`. */
|
|
28
|
+
const NO_CURATION_DATA = 'No curation data in this project yet.';
|
|
29
|
+
/**
|
|
30
|
+
* How long a writer here (--restore, --clear, --reset) waits for the curation lock
|
|
31
|
+
* before it refuses as busy. An op holds the lock for milliseconds, so a longer
|
|
32
|
+
* wait means a stuck holder, and the store's own 30 s wait would leave the command
|
|
33
|
+
* looking hung.
|
|
34
|
+
*/
|
|
35
|
+
const LOCK_WAIT_MS = 5000;
|
|
36
|
+
/**
|
|
37
|
+
* Code point ranges JSON.stringify leaves raw that a terminal may act on, or may
|
|
38
|
+
* display reordered: DEL and the C1 controls, the directional marks, the line and
|
|
39
|
+
* paragraph separators, and the bidirectional embeddings, overrides and isolates.
|
|
40
|
+
*/
|
|
41
|
+
const TERMINAL_UNSAFE_RANGES = [
|
|
42
|
+
[0x7f, 0x9f],
|
|
43
|
+
[0x200e, 0x200f],
|
|
44
|
+
[0x2028, 0x2029],
|
|
45
|
+
[0x202a, 0x202e],
|
|
46
|
+
[0x2066, 0x2069],
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* `value` as pretty JSON a terminal shows as written: every character in
|
|
50
|
+
* TERMINAL_UNSAFE_RANGES becomes its `\uXXXX` escape. Such characters can occur
|
|
51
|
+
* only inside JSON strings, where the escape means the same character, so the
|
|
52
|
+
* text still parses back to `value`.
|
|
53
|
+
*/
|
|
54
|
+
function terminalSafeJson(value) {
|
|
55
|
+
let text = '';
|
|
56
|
+
for (const ch of JSON.stringify(value, null, 2)) {
|
|
57
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
58
|
+
const unsafe = TERMINAL_UNSAFE_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
59
|
+
text += unsafe ? `\\u${code.toString(16).padStart(4, '0')}` : ch;
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
/** `1 decision`, `2 decisions`. */
|
|
64
|
+
function counted(count, noun) {
|
|
65
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The curation store (D-LEARNING-STORE-SEAM), or null after reporting why it
|
|
69
|
+
* cannot be used and setting exit code 1.
|
|
70
|
+
*/
|
|
71
|
+
function requireStore() {
|
|
72
|
+
const loaded = loadCurationStore();
|
|
73
|
+
if (loaded.ok)
|
|
74
|
+
return loaded.value;
|
|
75
|
+
p.log.error(`Curation: ${formatCurationStoreUnavailable(loaded.error)}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Which fixed notice a store refusal of kind `tracked` takes: the reason the store names
|
|
81
|
+
* on the refusal. A refusal that names none is `unknown`, the notice that claims least.
|
|
82
|
+
*/
|
|
83
|
+
function pauseReasonOf(error) {
|
|
84
|
+
return error.reason ?? 'unknown';
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The fixed line of a paused curation area at `ledgerRoot`, or null when git tracks
|
|
88
|
+
* nothing in the curation folders there. The area is paused when git tracks files in the
|
|
89
|
+
* curation folder, the old ledger folder or a backup folder, or when a `.git` entry stands
|
|
90
|
+
* and git did not answer; a root with no `.git` entry makes no git call. Every command
|
|
91
|
+
* asks first, before it reads a ledger file, takes a lock or prompts.
|
|
92
|
+
*/
|
|
93
|
+
async function pausedCurationLine(ledgerRoot) {
|
|
94
|
+
const { answer } = await probeTrackedArea(ledgerRoot, 'curation');
|
|
95
|
+
return answer === 'untracked' ? null : AREA_PAUSE_NOTICE.curation[answer];
|
|
96
|
+
}
|
|
97
|
+
/** Say that a command changed nothing because the curation area is paused: the fixed `line`, exit code 1. */
|
|
98
|
+
function refusePaused(line) {
|
|
99
|
+
p.log.error(line);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
/** What a writer says when the store refused; `undone` completes "Nothing was …". */
|
|
103
|
+
function storeRefusal(error, undone) {
|
|
104
|
+
switch (error.kind) {
|
|
105
|
+
case 'no-curation-dir': return NO_CURATION_DATA;
|
|
106
|
+
case 'tracked': return AREA_PAUSE_NOTICE.curation[pauseReasonOf(error)];
|
|
107
|
+
case 'busy': return `The curation store is busy: another run holds its lock. Nothing was ${undone}; try again in a moment.`;
|
|
108
|
+
default: return error.message;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ask `message` on a terminal and say whether to go on, logging `cancelled` when the
|
|
113
|
+
* answer is no. With no terminal there is no one to ask: a script that passed the
|
|
114
|
+
* flag has decided.
|
|
115
|
+
*/
|
|
116
|
+
async function confirmOnTerminal(message, cancelled) {
|
|
117
|
+
if (!process.stdin.isTTY)
|
|
118
|
+
return true;
|
|
119
|
+
const confirmed = await p.confirm({ message, initialValue: false });
|
|
120
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
121
|
+
p.log.info(cancelled);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/** True when `dir` is a directory; false when nothing, or something else, is there. */
|
|
127
|
+
async function isDirectory(dir) {
|
|
128
|
+
try {
|
|
129
|
+
return (await fs.stat(dir)).isDirectory();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const code = err.code;
|
|
133
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
134
|
+
return false;
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether the old ledger folder name is present under `root`'s `.devflow`: a folder, a
|
|
140
|
+
* file or a link, dangling or not. The cheap state check that keeps the store unloaded
|
|
141
|
+
* where there is nothing to move. A probe that fails for another reason reads as
|
|
142
|
+
* present, so the move itself reports the problem.
|
|
143
|
+
*/
|
|
144
|
+
async function oldLedgerFolderPresent(root) {
|
|
145
|
+
try {
|
|
146
|
+
await fs.lstat(path.join(root, '.devflow', LEGACY_NAMES.folder));
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const code = err.code;
|
|
151
|
+
return code !== 'ENOENT' && code !== 'ENOTDIR';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The lines a move that did not complete says: its own, or the store's reason when it could not load. */
|
|
155
|
+
function moveProblemLines(outcome) {
|
|
156
|
+
return outcome.kind === 'unavailable'
|
|
157
|
+
? [`The curation data was not moved: ${describeMoveText(outcome.message)}.`]
|
|
158
|
+
: describeOutcome(outcome);
|
|
159
|
+
}
|
|
160
|
+
/** The move notice of a read-only command, or null when there is none. The old ledger is never opened. */
|
|
161
|
+
async function moveReadNotice(ledgerRoot) {
|
|
162
|
+
const newFolderPresent = await isDirectory(getCurationDir(ledgerRoot)).catch(() => false);
|
|
163
|
+
const notice = gatherMoveNotice(ledgerRoot, os.homedir());
|
|
164
|
+
if (notice === null)
|
|
165
|
+
return null;
|
|
166
|
+
if (!newFolderPresent)
|
|
167
|
+
return { kind: 'instead', line: describeMoveNotice(notice) };
|
|
168
|
+
return isMoveSkipReason(notice) ? { kind: 'beside', line: describeMoveNotice(notice) } : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The gate every writing subcommand passes first (--restore, --clear, --reset and the
|
|
172
|
+
* project scope of --configure). A paused curation area (git tracks files in a data
|
|
173
|
+
* folder, or git did not answer) refuses with the fixed notice before anything else, so
|
|
174
|
+
* a clear or a reset never reaches its confirm prompt. Installed scripts of another
|
|
175
|
+
* version read and create another ledger folder, so they refuse with the skew line.
|
|
176
|
+
* Then, where the old folder is present, the shared move runs with no wait: a busy lock
|
|
177
|
+
* prints a retry message, a queue claim that holds the move prints its own message, and
|
|
178
|
+
* a refused or failed move prints its reason; in each case nothing is written.
|
|
179
|
+
* `undone` completes "Nothing was …". Answers true when the caller may write.
|
|
180
|
+
*/
|
|
181
|
+
async function prepareToWrite(ledgerRoot, undone) {
|
|
182
|
+
const paused = await pausedCurationLine(ledgerRoot);
|
|
183
|
+
if (paused !== null) {
|
|
184
|
+
refusePaused(paused);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const skew = await installedScriptsSkew(getDevFlowDirectory());
|
|
188
|
+
if (skew !== null) {
|
|
189
|
+
p.log.error(skew);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (!(await oldLedgerFolderPresent(ledgerRoot)))
|
|
194
|
+
return true;
|
|
195
|
+
const moved = await runSharedMove(ledgerRoot);
|
|
196
|
+
if (moved.kind === 'moved' || moved.kind === 'unchanged')
|
|
197
|
+
return true;
|
|
198
|
+
p.log.error(moved.kind === 'busy'
|
|
199
|
+
? (moved.reason === 'claim' ? claimHoldsMoveLine(undone) : movingLine(undone))
|
|
200
|
+
: describeMoveText(`${moveProblemLines(moved).join(' ')} Nothing was ${undone}.`));
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
/** The line a writer prints when a lock holds the move of the old folder. `undone` completes "Nothing was …". */
|
|
205
|
+
function movingLine(undone) {
|
|
206
|
+
return `Another run is moving this project's curation data. Nothing was ${undone}; try again in a moment.`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The line a writer prints when a Curate run holds the queue claim, which holds the move:
|
|
210
|
+
* no other run moves anything, and the claim lasts up to 15 minutes. `undone` completes "Nothing was …".
|
|
211
|
+
*/
|
|
212
|
+
function claimHoldsMoveLine(undone) {
|
|
213
|
+
return `A Curate run holds this project's curation queue, and the move waits for it. Nothing was ${undone}; try again after the run finishes (up to 15 minutes).`;
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Sub-command handlers
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function printUsage() {
|
|
219
|
+
p.intro(color.bgCyan(color.black(' Curation ')));
|
|
220
|
+
p.note(`${color.cyan('devflow curation --enable')} Enable curation in every project (a repository can opt out)\n` +
|
|
221
|
+
`${color.cyan('devflow curation --disable')} Disable curation in every project (drains this project's queue)\n` +
|
|
222
|
+
`${color.cyan('devflow curation --status')} Show curation status and entry counts\n` +
|
|
223
|
+
`${color.cyan('devflow curation --list')} List entries, inactive entries and observations\n` +
|
|
224
|
+
`${color.cyan('devflow curation --show <id>')} Print one entry or observation as JSON\n` +
|
|
225
|
+
`${color.cyan('devflow curation --restore <id>')} Make an inactive entry active again\n` +
|
|
226
|
+
`${color.cyan('devflow curation --configure')} Configuration wizard\n` +
|
|
227
|
+
`${color.cyan('devflow curation --clear')} Drop the observations no entry uses, and drain the queue\n` +
|
|
228
|
+
`${color.cyan('devflow curation --reset')} Remove all curation state files`, 'Usage');
|
|
229
|
+
p.outro(color.dim('Detects architectural decisions and known pitfalls from your sessions'));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve the ledger root for a state-mutating subcommand, warning and
|
|
233
|
+
* returning null if the caller isn't inside a git project. `actionSuffix`
|
|
234
|
+
* completes "Could not resolve git root — {actionSuffix}".
|
|
235
|
+
*
|
|
236
|
+
* D-LEDGER-MAIN-WORKTREE: every subcommand here resolves the ledger with
|
|
237
|
+
* getLedgerRoot — the hooks' DF_LEDGER_ROOT rule — so in a linked worktree it
|
|
238
|
+
* reads, clears and drains the main checkout's ledger the hooks write.
|
|
239
|
+
*/
|
|
240
|
+
async function requireLedgerRoot(actionSuffix) {
|
|
241
|
+
const ledgerRoot = await getLedgerRoot();
|
|
242
|
+
if (!ledgerRoot) {
|
|
243
|
+
p.log.warn(`Could not resolve git root — ${actionSuffix}`);
|
|
244
|
+
}
|
|
245
|
+
return ledgerRoot;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The --status entry lines: active entries by type, inactive ones by status (in
|
|
249
|
+
* the store's order, each status present), the active entries still in the v1
|
|
250
|
+
* format — the migration still to do — and the observations.
|
|
251
|
+
*/
|
|
252
|
+
function entryCountLines(listing, logRowCount, inactiveStatuses) {
|
|
253
|
+
const active = listing.active;
|
|
254
|
+
const decisions = active.filter(entry => entry.type === 'decision').length;
|
|
255
|
+
const pitfalls = active.filter(entry => entry.type === 'pitfall').length;
|
|
256
|
+
const byStatus = inactiveStatuses
|
|
257
|
+
.map(status => ({ status, count: listing.inactive.filter(entry => entry.status === status).length }))
|
|
258
|
+
.filter(({ count }) => count > 0)
|
|
259
|
+
.map(({ status, count }) => `${status} ${count}`);
|
|
260
|
+
const legacy = active.filter(entry => entry.schema === 1).length;
|
|
261
|
+
return [
|
|
262
|
+
`Entries: ${active.length} active (${counted(decisions, 'decision')}, ${counted(pitfalls, 'pitfall')}), `
|
|
263
|
+
+ `${listing.inactive.length} inactive${byStatus.length > 0 ? ` (${byStatus.join(', ')})` : ''}`,
|
|
264
|
+
`Legacy v1 entries: ${legacy} of ${active.length} active`,
|
|
265
|
+
`Observations: ${logRowCount} in the log, ${listing.observations.length} not yet promoted`,
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The `--status` backup lines: one per backup folder a move left under `.devflow`, with
|
|
270
|
+
* the rows its unswept queue files still hold, one more line for the folders past
|
|
271
|
+
* the listing limit (up to the 500th, which are read), and one for those past the 500th
|
|
272
|
+
* (not read, so their rows are not counted). A backup is inert, so the count tells the
|
|
273
|
+
* user when deleting it loses nothing. A count that fails is a warning, not an error.
|
|
274
|
+
*/
|
|
275
|
+
function backupLines(ledgerRoot) {
|
|
276
|
+
const mover = loadCurationMove();
|
|
277
|
+
if (!mover.ok)
|
|
278
|
+
return { lines: [], warning: `Backup rows were not counted: ${formatCurationStoreUnavailable(mover.error)}` };
|
|
279
|
+
const rows = mover.value.backupPendingRows(ledgerRoot);
|
|
280
|
+
if (!rows.ok)
|
|
281
|
+
return { lines: [], warning: `Backup rows were not counted: ${describeMoveText(rows.error.message)}` };
|
|
282
|
+
const lines = rows.value.backups.map(({ name, pendingRows }) => `Backup: ${describeMoveText(path.join(ledgerRoot, '.devflow', name))}: ${counted(pendingRows, 'pending row')} in the backup`);
|
|
283
|
+
const beyond = rows.value.beyondLimit;
|
|
284
|
+
if (beyond.backups > 0) {
|
|
285
|
+
const one = beyond.backups === 1;
|
|
286
|
+
lines.push(`Backup: ${counted(beyond.backups, 'more backup folder')} ${one ? 'is' : 'are'} not listed above: `
|
|
287
|
+
+ `${counted(beyond.pendingRows, 'pending row')} in ${one ? 'it' : 'them'}`);
|
|
288
|
+
}
|
|
289
|
+
if (beyond.unread > 0) {
|
|
290
|
+
const one = beyond.unread === 1;
|
|
291
|
+
lines.push(`Backup: ${counted(beyond.unread, 'more backup folder')} past the first 500 ${one ? 'is' : 'are'} not read: `
|
|
292
|
+
+ `${one ? 'its' : 'their'} pending rows are not counted`);
|
|
293
|
+
}
|
|
294
|
+
return { lines, warning: null };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* `--status`: the machine switch, then the counts the store reads. Reads only:
|
|
298
|
+
* malformed lines are counted, never quarantined, and no scope is checked. A project
|
|
299
|
+
* whose ledger still sits in the old folder gets the HUD's pointer (or refused line, or
|
|
300
|
+
* the notice of a skipped move) in place of the counts; a project with both folders and
|
|
301
|
+
* a skipped move gets the notice with the counts; a project with backup folders gets one
|
|
302
|
+
* line for each. A paused area (git tracks files in a data folder, or did not answer)
|
|
303
|
+
* gets the fixed notice after the machine switch, nothing is read, and the exit code is 0.
|
|
304
|
+
*/
|
|
305
|
+
async function handleStatus() {
|
|
306
|
+
// D-FEATURES-NARROW-ONLY: the machine switch is the manifest's and reads the
|
|
307
|
+
// same from every directory; a repository layer can only narrow it, and adds a
|
|
308
|
+
// line only when it does. The entry counts are per-project.
|
|
309
|
+
const enabled = await readMachineFeature(getDevFlowDirectory(), 'curation');
|
|
310
|
+
const settingsModule = loadSettingsModule();
|
|
311
|
+
const narrowed = enabled ? narrowedSwitchLabel(settingsModule, { dir: process.cwd() }, 'curation') : null;
|
|
312
|
+
const stateLine = `Curation: ${enabled ? 'enabled' : 'disabled'}`
|
|
313
|
+
+ (narrowed === null ? '' : `\nEffective here: ${narrowed}`);
|
|
314
|
+
const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
|
|
315
|
+
if (trackedWarning !== null)
|
|
316
|
+
p.log.warn(trackedWarning);
|
|
317
|
+
const ledgerRoot = await getLedgerRoot();
|
|
318
|
+
const paused = await pausedCurationLine(ledgerRoot ?? process.cwd());
|
|
319
|
+
if (paused !== null) {
|
|
320
|
+
p.log.info(`${stateLine}\n${paused}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!ledgerRoot) {
|
|
324
|
+
p.log.info(`${stateLine}\nEntries: not in a git project`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const notice = await moveReadNotice(ledgerRoot);
|
|
328
|
+
if (notice?.kind === 'instead') {
|
|
329
|
+
p.log.info(`${stateLine}\n${notice.line}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const head = notice === null ? stateLine : `${stateLine}\n${notice.line}`;
|
|
333
|
+
const loaded = loadCurationStore();
|
|
334
|
+
if (!loaded.ok) {
|
|
335
|
+
p.log.info(`${head}\nEntries: unavailable (${formatCurationStoreUnavailable(loaded.error)})`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const store = loaded.value;
|
|
339
|
+
const { ledgerRows, logRows, rejected } = store.readCurationState(ledgerRoot);
|
|
340
|
+
const listing = store.buildListing(ledgerRows, logRows, { rejected });
|
|
341
|
+
const backups = backupLines(ledgerRoot);
|
|
342
|
+
p.log.info([head, ...entryCountLines(listing, logRows.length, store.INACTIVE_STATUSES), ...backups.lines].join('\n'));
|
|
343
|
+
if (backups.warning !== null)
|
|
344
|
+
p.log.warn(backups.warning);
|
|
345
|
+
const { ledger, log } = listing.malformed;
|
|
346
|
+
if (ledger + log > 0) {
|
|
347
|
+
p.log.warn(`Malformed lines skipped: ${ledger} in the ledger, ${log} in the log. ` +
|
|
348
|
+
'The next op that rewrites a file moves its malformed lines to a .rejected.jsonl file beside it.');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* `--list`: the store's listing — the same text json-helper's `list` op prints —
|
|
353
|
+
* on stdout. Read-only. Outside a git project it reads the current directory. A paused
|
|
354
|
+
* area prints the fixed notice and exits 1, and reads nothing. Where the ledger still
|
|
355
|
+
* sits in the old folder it prints the pointer, the refused line or the notice of a
|
|
356
|
+
* skipped move in place of the listing; with both folders and a skipped move it prints
|
|
357
|
+
* the notice before the listing.
|
|
358
|
+
*/
|
|
359
|
+
async function handleList() {
|
|
360
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
361
|
+
const paused = await pausedCurationLine(root);
|
|
362
|
+
if (paused !== null) {
|
|
363
|
+
refusePaused(paused);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const notice = await moveReadNotice(root);
|
|
367
|
+
if (notice !== null)
|
|
368
|
+
p.log.info(notice.line);
|
|
369
|
+
if (notice?.kind === 'instead')
|
|
370
|
+
return;
|
|
371
|
+
const store = requireStore();
|
|
372
|
+
if (!store)
|
|
373
|
+
return;
|
|
374
|
+
const listed = store.readListing(root);
|
|
375
|
+
if (!listed.ok) {
|
|
376
|
+
if (listed.error.kind === 'no-curation-dir') {
|
|
377
|
+
p.log.info(NO_CURATION_DATA);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
p.log.error(listed.error.message);
|
|
381
|
+
process.exitCode = 1;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write(`${store.formatListing(listed.value)}\n`);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* `--show <id>`: one entry, by its anchor or its observation id, as the JSON
|
|
388
|
+
* json-helper's `show` op prints, made terminal-safe. Read-only. Outside a git
|
|
389
|
+
* project it reads the current directory. A paused area prints the fixed notice as the
|
|
390
|
+
* refusal and reads nothing. Where the ledger still sits in the old folder it prints the
|
|
391
|
+
* pointer, the refused line or the notice of a skipped move as the refusal; with both
|
|
392
|
+
* folders and a skipped move it prints the notice on stderr and goes on.
|
|
393
|
+
*/
|
|
394
|
+
async function handleShow(key) {
|
|
395
|
+
const root = (await getLedgerRoot()) ?? process.cwd();
|
|
396
|
+
const paused = await pausedCurationLine(root);
|
|
397
|
+
if (paused !== null) {
|
|
398
|
+
refusePaused(paused);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const notice = await moveReadNotice(root);
|
|
402
|
+
if (notice?.kind === 'instead') {
|
|
403
|
+
p.log.error(notice.line);
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// The JSON goes to stdout, so the notice of a skip goes to stderr.
|
|
408
|
+
if (notice !== null)
|
|
409
|
+
process.stderr.write(`${notice.line}\n`);
|
|
410
|
+
const store = requireStore();
|
|
411
|
+
if (!store)
|
|
412
|
+
return;
|
|
413
|
+
const shown = store.showByKey(root, key);
|
|
414
|
+
if (!shown.ok) {
|
|
415
|
+
p.log.error(shown.error.kind === 'no-curation-dir' ? NO_CURATION_DATA : shown.error.message);
|
|
416
|
+
process.exitCode = 1;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
process.stdout.write(`${terminalSafeJson(shown.value)}\n`);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* `--restore <id>`: make an inactive entry active again through the store's
|
|
423
|
+
* restoreAnchor, which clears its notes and its verification so maintenance
|
|
424
|
+
* reviews it again, and re-renders the files. It writes through the store lock, so
|
|
425
|
+
* it moves the old folder first (prepareToWrite). A refusal writes nothing.
|
|
426
|
+
*/
|
|
427
|
+
async function handleRestore(anchor) {
|
|
428
|
+
const ledgerRoot = await requireLedgerRoot('restore not performed');
|
|
429
|
+
if (!ledgerRoot) {
|
|
430
|
+
process.exitCode = 1;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const store = requireStore();
|
|
434
|
+
if (!store)
|
|
435
|
+
return;
|
|
436
|
+
if (!store.ANCHOR_ID_RE.test(anchor)) {
|
|
437
|
+
p.log.error(`--restore takes an entry id (ADR-NNN or PF-NNN), not ${JSON.stringify(anchor)}`);
|
|
438
|
+
process.exitCode = 1;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!(await prepareToWrite(ledgerRoot, 'restored')))
|
|
442
|
+
return;
|
|
443
|
+
const restored = store.restoreAnchor(ledgerRoot, anchor, { timeoutMs: LOCK_WAIT_MS });
|
|
444
|
+
if (!restored.ok) {
|
|
445
|
+
p.log.error(storeRefusal(restored.error, 'restored'));
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
p.log.success(`Restored ${restored.value.anchor_id} (${restored.value.status}); it is due for review again.`);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The scope choices of `devflow curation --configure`.
|
|
453
|
+
*
|
|
454
|
+
* D-LEARNING-MODEL-PRECEDENCE: the session-start hook takes the first layer that
|
|
455
|
+
* supplies a model — the project file, then a `devflow agents` Curate mapping,
|
|
456
|
+
* then the global file. A global file therefore has no effect for a user who has
|
|
457
|
+
* such a mapping, and its hint says so.
|
|
458
|
+
*/
|
|
459
|
+
export const CONFIGURE_SCOPE_OPTIONS = [
|
|
460
|
+
{ value: 'project', label: 'Project', hint: 'This project only (.devflow/curation/curation.json)' },
|
|
461
|
+
{
|
|
462
|
+
value: 'global',
|
|
463
|
+
label: 'Global',
|
|
464
|
+
hint: 'All projects (~/.devflow/curation.json); a devflow agents Curate mapping takes precedence over it',
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
/** The models `devflow curation --configure` offers, each with what it is good for. */
|
|
468
|
+
const CURATE_MODEL_CHOICES = [
|
|
469
|
+
{ value: 'opus', label: 'Opus', note: 'highest quality for detection + curation judgment' },
|
|
470
|
+
{ value: 'sonnet', label: 'Sonnet', note: 'good balance of quality and speed' },
|
|
471
|
+
{ value: 'haiku', label: 'Haiku', note: 'fastest, lowest cost' },
|
|
472
|
+
];
|
|
473
|
+
/**
|
|
474
|
+
* D-LEARNING-SHIPPED-ROW: the model offered as "Recommended" is the one the Curate
|
|
475
|
+
* agent ships with, the `model:` line of its frontmatter. It is read from there and
|
|
476
|
+
* written nowhere else, so a change of the shipped tier moves the recommendation with
|
|
477
|
+
* it and this list never has to be edited. A shipped model outside the three offered
|
|
478
|
+
* (or none) recommends nothing.
|
|
479
|
+
*/
|
|
480
|
+
export function curateModelOptions(shippedModel) {
|
|
481
|
+
return CURATE_MODEL_CHOICES.map(choice => ({
|
|
482
|
+
value: choice.value,
|
|
483
|
+
label: choice.label,
|
|
484
|
+
hint: choice.value === shippedModel
|
|
485
|
+
? `Recommended — ${choice.note}`
|
|
486
|
+
: choice.note.charAt(0).toUpperCase() + choice.note.slice(1),
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
async function handleConfigure() {
|
|
490
|
+
p.intro(color.bgCyan(color.black(' Curation Configuration ')));
|
|
491
|
+
const shippedModel = (await loadShippedAgentDefaults()).curate?.model;
|
|
492
|
+
const model = await p.select({
|
|
493
|
+
message: 'Model for decision detection',
|
|
494
|
+
options: curateModelOptions(shippedModel),
|
|
495
|
+
});
|
|
496
|
+
if (p.isCancel(model)) {
|
|
497
|
+
p.cancel('Configuration cancelled.');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const debugMode = await p.confirm({
|
|
501
|
+
message: 'Enable debug logging? (logs session content excerpts to ~/.devflow/logs/)',
|
|
502
|
+
initialValue: false,
|
|
503
|
+
});
|
|
504
|
+
if (p.isCancel(debugMode)) {
|
|
505
|
+
p.cancel('Configuration cancelled.');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const scope = await p.select({
|
|
509
|
+
message: 'Configuration scope',
|
|
510
|
+
options: [...CONFIGURE_SCOPE_OPTIONS],
|
|
511
|
+
});
|
|
512
|
+
if (p.isCancel(scope)) {
|
|
513
|
+
p.cancel('Configuration cancelled.');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const config = {
|
|
517
|
+
model,
|
|
518
|
+
debug: debugMode,
|
|
519
|
+
};
|
|
520
|
+
const configJson = JSON.stringify(config, null, 2) + '\n';
|
|
521
|
+
if (scope === 'global') {
|
|
522
|
+
const globalDir = getDevFlowDirectory();
|
|
523
|
+
await fs.mkdir(globalDir, { recursive: true });
|
|
524
|
+
const globalConfigPath = getGlobalCurateTuningConfigPath(globalDir);
|
|
525
|
+
await fs.writeFile(globalConfigPath, configJson, 'utf-8');
|
|
526
|
+
p.log.success(`Global config written to ${color.dim(globalConfigPath)}`);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// D-LEDGER-MAIN-WORKTREE: session-start-context reads the project tuning config
|
|
530
|
+
// from the ledger ($LEDGER_ROOT/.devflow/curation/), so write it there — the
|
|
531
|
+
// main checkout in a linked worktree; the current directory outside git.
|
|
532
|
+
const projectRoot = (await getLedgerRoot()) ?? process.cwd();
|
|
533
|
+
// Skew or a move that cannot run (busy, refused, failed) writes nothing, so the
|
|
534
|
+
// config never lands in a folder the readers do not use.
|
|
535
|
+
if (!(await prepareToWrite(projectRoot, 'written')))
|
|
536
|
+
return;
|
|
537
|
+
// The creator rule: this writer makes the new folder only when the old name is
|
|
538
|
+
// absent, so it looks once more. A folder of the old name can stand again after the
|
|
539
|
+
// move, and the new folder made beside it would hide its ledger from every reader.
|
|
540
|
+
if (await oldLedgerFolderPresent(projectRoot)) {
|
|
541
|
+
p.log.error(movingLine('written'));
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// The folder that is checked is the folder that is written.
|
|
546
|
+
const curationDir = getCurationDir(projectRoot);
|
|
547
|
+
const projectConfigPath = path.join(curationDir, CURATION_TUNING_FILE_NAME);
|
|
548
|
+
// D-CLI-NO-SYMLINK (core/linked-path.ts): nothing is written through a .devflow,
|
|
549
|
+
// a curation folder or a curation.json that is a symbolic link.
|
|
550
|
+
const linked = await firstSymbolicLink([path.dirname(curationDir), curationDir, projectConfigPath]);
|
|
551
|
+
if (linked !== null) {
|
|
552
|
+
p.log.error(`Project config not written: ${linked} is a symbolic link, and devflow writes nothing through one`);
|
|
553
|
+
process.exitCode = 1;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
await fs.mkdir(curationDir, { recursive: true });
|
|
557
|
+
await fs.writeFile(projectConfigPath, configJson, 'utf-8');
|
|
558
|
+
p.log.success(`Project config written to ${color.dim(projectConfigPath)}`);
|
|
559
|
+
}
|
|
560
|
+
p.outro(color.green('Configuration saved.'));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* `--reset`: remove all curation state — entries, observations, rendered files,
|
|
564
|
+
* the tuning config, and the queue with its claim — through the store's
|
|
565
|
+
* resetCuration, under the curation lock every curation writer takes
|
|
566
|
+
* (D-ONE-LEARNING-LOCK, D-RESET-UNDER-LOCK). The old folder moves first
|
|
567
|
+
* (prepareToWrite), so the reset reaches the data the hooks use; a backup folder a
|
|
568
|
+
* move left stays in place. A project with no curation directory has nothing to
|
|
569
|
+
* reset, and nothing is created there (D-NO-STRAY-TREE).
|
|
570
|
+
*/
|
|
571
|
+
async function handleReset() {
|
|
572
|
+
const ledgerRoot = await requireLedgerRoot('reset not performed');
|
|
573
|
+
if (!ledgerRoot)
|
|
574
|
+
return;
|
|
575
|
+
const store = requireStore();
|
|
576
|
+
if (!store)
|
|
577
|
+
return;
|
|
578
|
+
if (!(await prepareToWrite(ledgerRoot, 'reset')))
|
|
579
|
+
return;
|
|
580
|
+
if (!(await isDirectory(getCurationDir(ledgerRoot)))) {
|
|
581
|
+
p.log.info('No curation data to reset.');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const proceed = await confirmOnTerminal('Remove all curation state files? This cannot be undone.', 'Reset cancelled.');
|
|
585
|
+
if (!proceed)
|
|
586
|
+
return;
|
|
587
|
+
const reset = store.resetCuration(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
588
|
+
if (!reset.ok) {
|
|
589
|
+
if (reset.error.kind === 'no-curation-dir') {
|
|
590
|
+
p.log.info('No curation data to reset.');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
p.log.error(storeRefusal(reset.error, 'reset'));
|
|
594
|
+
process.exitCode = 1;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
p.log.success('Reset complete — removed .devflow/curation/ state.');
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* `--clear`: drop the observations no entry uses (D-CLEAR-UNREFERENCED), then
|
|
601
|
+
* drain the curation queue. The queue is drained only once the clear succeeded:
|
|
602
|
+
* a busy lock or a refusal leaves both the log and the queue as they were, so a
|
|
603
|
+
* failed clear changes nothing. The old folder moves first (prepareToWrite), so the
|
|
604
|
+
* clear and the drain reach the data the hooks use; a backup folder a move left
|
|
605
|
+
* stays in place. When git tracks any entry under a data folder or a backup folder, or
|
|
606
|
+
* does not answer, the drain deletes no queue file and a warning carries the fixed
|
|
607
|
+
* notice.
|
|
608
|
+
*/
|
|
609
|
+
async function handleClear() {
|
|
610
|
+
const ledgerRoot = await requireLedgerRoot('clear not performed');
|
|
611
|
+
if (!ledgerRoot)
|
|
612
|
+
return;
|
|
613
|
+
const store = requireStore();
|
|
614
|
+
if (!store)
|
|
615
|
+
return;
|
|
616
|
+
if (!(await prepareToWrite(ledgerRoot, 'cleared')))
|
|
617
|
+
return;
|
|
618
|
+
if (!(await isDirectory(getCurationDir(ledgerRoot)))) {
|
|
619
|
+
p.log.info('No curation data to clear.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const proceed = await confirmOnTerminal('Drop every observation no entry uses? Entries and the observations they use are kept. This cannot be undone.', 'Clear cancelled.');
|
|
623
|
+
if (!proceed)
|
|
624
|
+
return;
|
|
625
|
+
const cleared = store.clearUnreferenced(ledgerRoot, { timeoutMs: LOCK_WAIT_MS });
|
|
626
|
+
if (!cleared.ok) {
|
|
627
|
+
if (cleared.error.kind === 'no-curation-dir') {
|
|
628
|
+
p.log.info('No curation data to clear.');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
p.log.error(storeRefusal(cleared.error, 'cleared'));
|
|
632
|
+
process.exitCode = 1;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// A mid-run Curate agent whose claimed batch vanishes stops without further
|
|
636
|
+
// writes — the desired outcome of clearing.
|
|
637
|
+
const drain = await drainCurationQueue(ledgerRoot);
|
|
638
|
+
p.log.success(`Cleared ${counted(cleared.value.cleared, 'observation')} no entry uses and kept ${cleared.value.kept} ` +
|
|
639
|
+
`that entries use${drain.drained ? '; drained the curation queue.' : '.'}`);
|
|
640
|
+
if (!drain.drained)
|
|
641
|
+
p.log.warn(formatCurationNotDrained(drain));
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Make the installed prompts match the switch just written
|
|
645
|
+
* (D-LEARNING-VARIANT-INSTALL): the curation-on or curation-off commands and
|
|
646
|
+
* agents, and the apply-decisions skill. The same convergence `devflow init` runs,
|
|
647
|
+
* followed by reapplying the saved agent model mapping so `devflow agents`
|
|
648
|
+
* overrides survive.
|
|
649
|
+
*
|
|
650
|
+
* Warn-not-fail: the switch itself is already recorded, and either mismatch is
|
|
651
|
+
* safe (an on variant is still gated at run time, an off variant loads no
|
|
652
|
+
* decisions), so a problem here never changes the exit code.
|
|
653
|
+
*
|
|
654
|
+
* Answers whether the converge ran, which means the installed files are the ones
|
|
655
|
+
* this CLI writes. The toggle moves the ledger folder only then: under version skew
|
|
656
|
+
* (or when that could not be told) it moves nothing.
|
|
657
|
+
*/
|
|
658
|
+
async function convergeInstalledVariants(enabled) {
|
|
659
|
+
try {
|
|
660
|
+
const outcome = await applyCurationToggle({
|
|
661
|
+
claudeDir: getClaudeDirectory(),
|
|
662
|
+
devflowDir: getDevFlowDirectory(),
|
|
663
|
+
curation: enabled,
|
|
664
|
+
runningVersion: await readRunningVersion(),
|
|
665
|
+
warn: (msg) => p.log.warn(msg),
|
|
666
|
+
});
|
|
667
|
+
if (outcome.kind === 'skipped') {
|
|
668
|
+
p.log.warn(outcome.message);
|
|
669
|
+
return 'not-converged';
|
|
670
|
+
}
|
|
671
|
+
const changed = describeCurationConverge(outcome.result, enabled);
|
|
672
|
+
if (changed !== null)
|
|
673
|
+
p.log.info(color.dim(`Installed prompts: ${changed}`));
|
|
674
|
+
return 'converged';
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
p.log.warn(`Could not update the installed prompts for the new curation setting — run ${color.cyan('devflow init')}: ${String(err)}`);
|
|
678
|
+
return 'not-converged';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* The toggle's move of the current project's ledger folder: tried once with no wait,
|
|
683
|
+
* and only where the old folder is present. A busy move is not an error — the project
|
|
684
|
+
* moves at its next event — and a refusal, a failure or a skip (git tracks files in a
|
|
685
|
+
* data folder, or git did not answer) is a warning: the switch is already recorded, so
|
|
686
|
+
* none of them changes the exit code. Answers the skip reason whose fixed notice it
|
|
687
|
+
* printed, or null when it printed none.
|
|
688
|
+
*/
|
|
689
|
+
async function tryMoveForToggle(ledgerRoot) {
|
|
690
|
+
if (!(await oldLedgerFolderPresent(ledgerRoot)))
|
|
691
|
+
return null;
|
|
692
|
+
const outcome = await runSharedMove(ledgerRoot);
|
|
693
|
+
if (outcome.kind === 'moved' || outcome.kind === 'unchanged')
|
|
694
|
+
return null;
|
|
695
|
+
for (const line of moveProblemLines(outcome)) {
|
|
696
|
+
if (outcome.kind === 'busy')
|
|
697
|
+
p.log.info(color.dim(describeMoveText(line)));
|
|
698
|
+
else
|
|
699
|
+
p.log.warn(describeMoveText(line));
|
|
700
|
+
}
|
|
701
|
+
return outcome.kind === 'left-alone' ? outcome.reason : null;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* `--enable` / `--disable`: the machine-wide switch (D-FEATURES-NARROW-ONLY),
|
|
705
|
+
* converged exactly as `devflow init --curation / --no-curation` converges it —
|
|
706
|
+
* the manifest value (both machine keys, written first), the installed prompt
|
|
707
|
+
* variants and skill (D-LEARNING-VARIANT-INSTALL), the current project's ledger
|
|
708
|
+
* folder move, and on disable a drained queue in every ledger folder that exists and
|
|
709
|
+
* in the backups a move left. The drain deletes no queue file, and prints the fixed
|
|
710
|
+
* pause notice of the curation area, when git tracks any entry under a data folder or a
|
|
711
|
+
* backup folder, or does not answer. A move that has just printed its own notice for the
|
|
712
|
+
* same reason leaves the drain's line short, so the cause is told once.
|
|
713
|
+
* Never requires a git root: the switch is not a per-project setting.
|
|
714
|
+
*
|
|
715
|
+
* Under version skew the toggle records the switch and moves nothing; otherwise it
|
|
716
|
+
* tries the shared move with no wait. Only `--disable` drains, skew included.
|
|
717
|
+
*/
|
|
718
|
+
async function handleToggle(enabled) {
|
|
719
|
+
const recorded = await writeMachineFeature(getDevFlowDirectory(), 'curation', enabled);
|
|
720
|
+
if (!recorded.ok) {
|
|
721
|
+
p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
|
|
722
|
+
process.exitCode = 1;
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const converged = await convergeInstalledVariants(enabled);
|
|
726
|
+
const ledgerRoot = await getLedgerRoot();
|
|
727
|
+
const shownSkip = converged === 'converged' && ledgerRoot ? await tryMoveForToggle(ledgerRoot) : null;
|
|
728
|
+
if (enabled) {
|
|
729
|
+
p.log.success('Curation enabled in every project (a repository can opt out)');
|
|
730
|
+
p.log.info(color.dim('Architectural decisions and pitfalls will be detected from your sessions'));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Drain the current project's curation (decisions-detection) queue, in each ledger
|
|
734
|
+
// folder that exists and in the backups a move left, so stale turns don't process on
|
|
735
|
+
// re-enable. A mid-run Curate agent whose claimed batch vanishes aborts without
|
|
736
|
+
// changes — the desired outcome of disabling. A project whose data folders git tracks
|
|
737
|
+
// keeps every file: the switch is recorded, and the drain warns instead.
|
|
738
|
+
if (ledgerRoot) {
|
|
739
|
+
const drain = await drainDisabledCurationQueue(ledgerRoot);
|
|
740
|
+
if (!drain.drained)
|
|
741
|
+
p.log.warn(formatCurationNotDrained(drain, shownSkip));
|
|
742
|
+
else if (drain.warning !== undefined)
|
|
743
|
+
p.log.warn(drain.warning);
|
|
744
|
+
}
|
|
745
|
+
p.log.success('Curation disabled in every project');
|
|
746
|
+
}
|
|
747
|
+
export const curationCommand = new Command('curation')
|
|
748
|
+
.description('Enable or disable curation (decision/pitfall detection) in every project')
|
|
749
|
+
.option('--enable', 'Enable curation in every project (a repository can opt out)')
|
|
750
|
+
.option('--disable', 'Disable curation in every project')
|
|
751
|
+
.option('--status', 'Show curation status and entry counts')
|
|
752
|
+
.option('--list', 'List entries, inactive entries with their notes, and observations')
|
|
753
|
+
.option('--show <id>', 'Print one entry (ADR-NNN or PF-NNN) or observation (obs_...) as JSON')
|
|
754
|
+
.option('--restore <id>', 'Make an inactive entry active again')
|
|
755
|
+
.option('--configure', 'Interactive configuration wizard for curation.json')
|
|
756
|
+
.option('--clear', 'Drop the observations no entry uses, and drain the curation queue')
|
|
757
|
+
.option('--reset', 'Remove all curation state files and artifacts')
|
|
758
|
+
.action(async (options) => {
|
|
759
|
+
const knownFlags = [
|
|
760
|
+
'enable', 'disable', 'status', 'list', 'show', 'restore', 'configure',
|
|
761
|
+
'clear', 'reset',
|
|
762
|
+
];
|
|
763
|
+
const hasFlag = knownFlags.some((f) => options[f] !== undefined);
|
|
764
|
+
if (!hasFlag) {
|
|
765
|
+
printUsage();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Thin router — the read-only commands first (status, list, show), then
|
|
769
|
+
// configure, reset, clear, restore, enable, disable. Each handler owns its own
|
|
770
|
+
// path resolution and I/O.
|
|
771
|
+
if (options.status) {
|
|
772
|
+
await handleStatus();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (options.list) {
|
|
776
|
+
await handleList();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (options.show !== undefined) {
|
|
780
|
+
await handleShow(options.show);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (options.configure) {
|
|
784
|
+
await handleConfigure();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (options.reset) {
|
|
788
|
+
await handleReset();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (options.clear) {
|
|
792
|
+
await handleClear();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (options.restore !== undefined) {
|
|
796
|
+
await handleRestore(options.restore);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (options.enable) {
|
|
800
|
+
await handleToggle(true);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (options.disable) {
|
|
804
|
+
await handleToggle(false);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
//# sourceMappingURL=curation.js.map
|