@lumenflow/cli 2.2.2 → 2.3.2
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/README.md +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/release.test.js +61 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +356 -101
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +517 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/release.js +69 -29
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +4 -3
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +4 -3
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +39 -12
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/**
|
|
4
|
+
* Unified State Cleanup CLI (WU-1208)
|
|
5
|
+
*
|
|
6
|
+
* Single command to orchestrate all state cleanup operations:
|
|
7
|
+
* - Signal cleanup (TTL-based pruning)
|
|
8
|
+
* - Memory cleanup (lifecycle-based pruning)
|
|
9
|
+
* - Event archival (age-based archiving)
|
|
10
|
+
*
|
|
11
|
+
* Cleanup order: signals -> memory -> events (dependency order)
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Respects config from .lumenflow.config.yaml
|
|
15
|
+
* - Supports --dry-run for preview
|
|
16
|
+
* - Supports --signals-only, --memory-only, --events-only for selective cleanup
|
|
17
|
+
* - Non-fatal: warns on errors but continues with other cleanups
|
|
18
|
+
* - Summary output shows removed/retained counts for each type
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* pnpm state:cleanup # Full cleanup: signals -> memory -> events
|
|
22
|
+
* pnpm state:cleanup --dry-run # Preview without changes
|
|
23
|
+
* pnpm state:cleanup --signals-only # Only signal cleanup
|
|
24
|
+
* pnpm state:cleanup --memory-only # Only memory cleanup
|
|
25
|
+
* pnpm state:cleanup --events-only # Only event archival
|
|
26
|
+
* pnpm state:cleanup --json # Output as JSON
|
|
27
|
+
*
|
|
28
|
+
* @see {@link packages/@lumenflow/core/src/state-cleanup-core.ts} - Core orchestration
|
|
29
|
+
* @see {@link packages/@lumenflow/core/src/__tests__/state-cleanup-core.test.ts} - Tests
|
|
30
|
+
*/
|
|
31
|
+
import fs from 'node:fs/promises';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { cleanupSignals } from '@lumenflow/memory/dist/signal-cleanup-core.js';
|
|
34
|
+
import { cleanupMemory } from '@lumenflow/memory/dist/mem-cleanup-core.js';
|
|
35
|
+
import { archiveWuEvents } from '@lumenflow/core/dist/wu-events-cleanup.js';
|
|
36
|
+
import { cleanupState } from '@lumenflow/core/dist/state-cleanup-core.js';
|
|
37
|
+
import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
|
|
38
|
+
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
39
|
+
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
40
|
+
import fg from 'fast-glob';
|
|
41
|
+
import { parse as parseYaml } from 'yaml';
|
|
42
|
+
/**
|
|
43
|
+
* Log prefix for state:cleanup output
|
|
44
|
+
*/
|
|
45
|
+
const LOG_PREFIX = '[state:cleanup]';
|
|
46
|
+
/**
|
|
47
|
+
* Tool name for audit logging
|
|
48
|
+
*/
|
|
49
|
+
const TOOL_NAME = 'state:cleanup';
|
|
50
|
+
/**
|
|
51
|
+
* Bytes per KB for formatting
|
|
52
|
+
*/
|
|
53
|
+
const BYTES_PER_KB = 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Active WU statuses that should protect signals
|
|
56
|
+
*/
|
|
57
|
+
const ACTIVE_WU_STATUSES = ['in_progress', 'blocked'];
|
|
58
|
+
/**
|
|
59
|
+
* Labels for output formatting
|
|
60
|
+
*/
|
|
61
|
+
const OUTPUT_LABELS = {
|
|
62
|
+
BREAKDOWN: ' Breakdown:',
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* CLI argument options specific to state:cleanup
|
|
66
|
+
*/
|
|
67
|
+
const CLI_OPTIONS = {
|
|
68
|
+
dryRun: {
|
|
69
|
+
name: 'dryRun',
|
|
70
|
+
flags: '--dry-run',
|
|
71
|
+
description: 'Preview cleanup without making changes',
|
|
72
|
+
},
|
|
73
|
+
signalsOnly: {
|
|
74
|
+
name: 'signalsOnly',
|
|
75
|
+
flags: '--signals-only',
|
|
76
|
+
description: 'Only execute signal cleanup',
|
|
77
|
+
},
|
|
78
|
+
memoryOnly: {
|
|
79
|
+
name: 'memoryOnly',
|
|
80
|
+
flags: '--memory-only',
|
|
81
|
+
description: 'Only execute memory cleanup',
|
|
82
|
+
},
|
|
83
|
+
eventsOnly: {
|
|
84
|
+
name: 'eventsOnly',
|
|
85
|
+
flags: '--events-only',
|
|
86
|
+
description: 'Only execute event archival',
|
|
87
|
+
},
|
|
88
|
+
json: {
|
|
89
|
+
name: 'json',
|
|
90
|
+
flags: '--json',
|
|
91
|
+
description: 'Output as JSON',
|
|
92
|
+
},
|
|
93
|
+
quiet: {
|
|
94
|
+
name: 'quiet',
|
|
95
|
+
flags: '-q, --quiet',
|
|
96
|
+
description: 'Suppress output except errors and summary',
|
|
97
|
+
},
|
|
98
|
+
baseDir: {
|
|
99
|
+
name: 'baseDir',
|
|
100
|
+
flags: '-b, --base-dir <path>',
|
|
101
|
+
description: 'Base directory (defaults to current directory)',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Write audit log entry for tool execution
|
|
106
|
+
*
|
|
107
|
+
* @param baseDir - Base directory
|
|
108
|
+
* @param entry - Audit log entry
|
|
109
|
+
*/
|
|
110
|
+
async function writeAuditLog(baseDir, entry) {
|
|
111
|
+
try {
|
|
112
|
+
const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
|
|
113
|
+
const logDir = path.dirname(logPath);
|
|
114
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
115
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
116
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
117
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
|
|
118
|
+
await fs.appendFile(logPath, line, 'utf-8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Audit logging is non-fatal - silently ignore errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format bytes as human-readable string
|
|
126
|
+
*
|
|
127
|
+
* @param bytes - Number of bytes
|
|
128
|
+
* @returns Formatted string (e.g., "1.5 KB")
|
|
129
|
+
*/
|
|
130
|
+
function formatBytes(bytes) {
|
|
131
|
+
if (bytes < BYTES_PER_KB) {
|
|
132
|
+
return `${bytes} B`;
|
|
133
|
+
}
|
|
134
|
+
const kb = (bytes / BYTES_PER_KB).toFixed(1);
|
|
135
|
+
return `${kb} KB`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get active WU IDs (in_progress or blocked) by scanning WU YAML files.
|
|
139
|
+
*
|
|
140
|
+
* @param baseDir - Base directory
|
|
141
|
+
* @returns Set of active WU IDs
|
|
142
|
+
*/
|
|
143
|
+
async function getActiveWuIds(baseDir) {
|
|
144
|
+
const activeIds = new Set();
|
|
145
|
+
try {
|
|
146
|
+
const config = getConfig({ projectRoot: baseDir });
|
|
147
|
+
const wuDir = path.join(baseDir, config.directories.wuDir);
|
|
148
|
+
// Find all WU YAML files
|
|
149
|
+
const wuFiles = await fg('WU-*.yaml', { cwd: wuDir });
|
|
150
|
+
for (const file of wuFiles) {
|
|
151
|
+
try {
|
|
152
|
+
const filePath = path.join(wuDir, file);
|
|
153
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
|
|
154
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
155
|
+
const wu = parseYaml(content);
|
|
156
|
+
if (wu.id && wu.status && ACTIVE_WU_STATUSES.includes(wu.status)) {
|
|
157
|
+
activeIds.add(wu.id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Skip files that fail to parse
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// If we can't read WU files, return empty set (safer to remove nothing)
|
|
168
|
+
}
|
|
169
|
+
return activeIds;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parse CLI arguments
|
|
173
|
+
*
|
|
174
|
+
* @returns Parsed arguments
|
|
175
|
+
*/
|
|
176
|
+
function parseArguments() {
|
|
177
|
+
return createWUParser({
|
|
178
|
+
name: 'state-cleanup',
|
|
179
|
+
description: 'Orchestrate all state cleanup: signals, memory, events',
|
|
180
|
+
options: [
|
|
181
|
+
CLI_OPTIONS.dryRun,
|
|
182
|
+
CLI_OPTIONS.signalsOnly,
|
|
183
|
+
CLI_OPTIONS.memoryOnly,
|
|
184
|
+
CLI_OPTIONS.eventsOnly,
|
|
185
|
+
CLI_OPTIONS.json,
|
|
186
|
+
CLI_OPTIONS.quiet,
|
|
187
|
+
CLI_OPTIONS.baseDir,
|
|
188
|
+
],
|
|
189
|
+
required: [],
|
|
190
|
+
allowPositionalId: false,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Print cleanup result to console
|
|
195
|
+
*
|
|
196
|
+
* @param result - State cleanup result
|
|
197
|
+
* @param quiet - Suppress verbose output
|
|
198
|
+
*/
|
|
199
|
+
function printResult(result, quiet) {
|
|
200
|
+
const mode = result.dryRun ? 'Dry-run' : 'Cleanup';
|
|
201
|
+
console.log(`${LOG_PREFIX} ${mode} complete`);
|
|
202
|
+
if (quiet) {
|
|
203
|
+
console.log(`Total: ${formatBytes(result.summary.totalBytesFreed)} freed, ` +
|
|
204
|
+
`executed: [${result.summary.typesExecuted.join(', ')}]`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log('=== Summary ===');
|
|
209
|
+
console.log(` Total Bytes Freed: ${formatBytes(result.summary.totalBytesFreed)}`);
|
|
210
|
+
console.log(` Types Executed: [${result.summary.typesExecuted.join(', ')}]`);
|
|
211
|
+
if (result.summary.typesSkipped.length > 0) {
|
|
212
|
+
console.log(` Types Skipped: [${result.summary.typesSkipped.join(', ')}]`);
|
|
213
|
+
}
|
|
214
|
+
// Signals section
|
|
215
|
+
if (result.signals) {
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log('--- Signals ---');
|
|
218
|
+
console.log(` Removed: ${result.signals.removedCount}`);
|
|
219
|
+
console.log(` Retained: ${result.signals.retainedCount}`);
|
|
220
|
+
console.log(` Freed: ${formatBytes(result.signals.bytesFreed)}`);
|
|
221
|
+
console.log(OUTPUT_LABELS.BREAKDOWN);
|
|
222
|
+
console.log(` TTL Expired (read): ${result.signals.breakdown.ttlExpired}`);
|
|
223
|
+
console.log(` TTL Expired (unread): ${result.signals.breakdown.unreadTtlExpired}`);
|
|
224
|
+
console.log(` Count Limit Exceeded: ${result.signals.breakdown.countLimitExceeded}`);
|
|
225
|
+
console.log(` Active WU Protected: ${result.signals.breakdown.activeWuProtected}`);
|
|
226
|
+
}
|
|
227
|
+
// Memory section
|
|
228
|
+
if (result.memory) {
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log('--- Memory ---');
|
|
231
|
+
console.log(` Removed: ${result.memory.removedCount}`);
|
|
232
|
+
console.log(` Retained: ${result.memory.retainedCount}`);
|
|
233
|
+
console.log(` Freed: ${formatBytes(result.memory.bytesFreed)}`);
|
|
234
|
+
console.log(OUTPUT_LABELS.BREAKDOWN);
|
|
235
|
+
console.log(` Ephemeral: ${result.memory.breakdown.ephemeral}`);
|
|
236
|
+
console.log(` Session: ${result.memory.breakdown.session}`);
|
|
237
|
+
console.log(` WU (summarized): ${result.memory.breakdown.wu}`);
|
|
238
|
+
console.log(` Sensitive (retained): ${result.memory.breakdown.sensitive}`);
|
|
239
|
+
console.log(` TTL Expired: ${result.memory.breakdown.ttlExpired}`);
|
|
240
|
+
console.log(` Active Session: ${result.memory.breakdown.activeSessionProtected}`);
|
|
241
|
+
}
|
|
242
|
+
// Events section
|
|
243
|
+
if (result.events) {
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log('--- Events ---');
|
|
246
|
+
console.log(` Archived WUs: ${result.events.archivedWuCount}`);
|
|
247
|
+
console.log(` Retained WUs: ${result.events.retainedWuCount}`);
|
|
248
|
+
console.log(` Archived Events: ${result.events.archivedEventCount}`);
|
|
249
|
+
console.log(` Retained Events: ${result.events.retainedEventCount}`);
|
|
250
|
+
console.log(` Archived: ${formatBytes(result.events.bytesArchived)}`);
|
|
251
|
+
console.log(OUTPUT_LABELS.BREAKDOWN);
|
|
252
|
+
console.log(` Older Than Threshold: ${result.events.breakdown.archivedOlderThanThreshold}`);
|
|
253
|
+
console.log(` Active WU Protected: ${result.events.breakdown.retainedActiveWu}`);
|
|
254
|
+
console.log(` Within Retention: ${result.events.breakdown.retainedWithinThreshold}`);
|
|
255
|
+
}
|
|
256
|
+
// Errors section
|
|
257
|
+
if (result.errors.length > 0) {
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log('=== Errors ===');
|
|
260
|
+
for (const error of result.errors) {
|
|
261
|
+
console.log(` [${error.type}] ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (result.dryRun) {
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log('To execute, run without --dry-run');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Main CLI entry point
|
|
271
|
+
*/
|
|
272
|
+
async function main() {
|
|
273
|
+
const args = parseArguments();
|
|
274
|
+
const baseDir = args.baseDir || process.cwd();
|
|
275
|
+
const startedAt = new Date().toISOString();
|
|
276
|
+
const startTime = Date.now();
|
|
277
|
+
// Validate mutually exclusive flags
|
|
278
|
+
const exclusiveFlags = [args.signalsOnly, args.memoryOnly, args.eventsOnly].filter(Boolean);
|
|
279
|
+
if (exclusiveFlags.length > 1) {
|
|
280
|
+
console.error(`${LOG_PREFIX} Error: --signals-only, --memory-only, and --events-only are mutually exclusive`);
|
|
281
|
+
process.exit(EXIT_CODES.ERROR);
|
|
282
|
+
}
|
|
283
|
+
let result = null;
|
|
284
|
+
let error = null;
|
|
285
|
+
try {
|
|
286
|
+
result = await cleanupState(baseDir, {
|
|
287
|
+
dryRun: args.dryRun,
|
|
288
|
+
signalsOnly: args.signalsOnly,
|
|
289
|
+
memoryOnly: args.memoryOnly,
|
|
290
|
+
eventsOnly: args.eventsOnly,
|
|
291
|
+
// Inject real cleanup functions
|
|
292
|
+
cleanupSignals: async (dir, opts) => cleanupSignals(dir, {
|
|
293
|
+
dryRun: opts.dryRun,
|
|
294
|
+
getActiveWuIds: () => getActiveWuIds(dir),
|
|
295
|
+
}),
|
|
296
|
+
cleanupMemory: async (dir, opts) => cleanupMemory(dir, {
|
|
297
|
+
dryRun: opts.dryRun,
|
|
298
|
+
}),
|
|
299
|
+
archiveEvents: async (dir, opts) => archiveWuEvents(dir, {
|
|
300
|
+
dryRun: opts.dryRun,
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
error = err.message;
|
|
306
|
+
}
|
|
307
|
+
const durationMs = Date.now() - startTime;
|
|
308
|
+
// Determine audit log status
|
|
309
|
+
let auditStatus = 'partial';
|
|
310
|
+
if (error) {
|
|
311
|
+
auditStatus = 'failed';
|
|
312
|
+
}
|
|
313
|
+
else if (result?.success) {
|
|
314
|
+
auditStatus = 'success';
|
|
315
|
+
}
|
|
316
|
+
await writeAuditLog(baseDir, {
|
|
317
|
+
tool: TOOL_NAME,
|
|
318
|
+
status: auditStatus,
|
|
319
|
+
startedAt,
|
|
320
|
+
completedAt: new Date().toISOString(),
|
|
321
|
+
durationMs,
|
|
322
|
+
input: {
|
|
323
|
+
baseDir,
|
|
324
|
+
dryRun: args.dryRun,
|
|
325
|
+
signalsOnly: args.signalsOnly,
|
|
326
|
+
memoryOnly: args.memoryOnly,
|
|
327
|
+
eventsOnly: args.eventsOnly,
|
|
328
|
+
},
|
|
329
|
+
output: result
|
|
330
|
+
? {
|
|
331
|
+
success: result.success,
|
|
332
|
+
totalBytesFreed: result.summary.totalBytesFreed,
|
|
333
|
+
typesExecuted: result.summary.typesExecuted,
|
|
334
|
+
typesSkipped: result.summary.typesSkipped,
|
|
335
|
+
errorCount: result.errors.length,
|
|
336
|
+
dryRun: result.dryRun,
|
|
337
|
+
}
|
|
338
|
+
: null,
|
|
339
|
+
error: error ? { message: error } : null,
|
|
340
|
+
});
|
|
341
|
+
if (error) {
|
|
342
|
+
console.error(`${LOG_PREFIX} Error: ${error}`);
|
|
343
|
+
process.exit(EXIT_CODES.ERROR);
|
|
344
|
+
}
|
|
345
|
+
if (args.json) {
|
|
346
|
+
console.log(JSON.stringify(result, null, 2));
|
|
347
|
+
process.exit(result?.success ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
348
|
+
}
|
|
349
|
+
if (result) {
|
|
350
|
+
printResult(result, args.quiet ?? false);
|
|
351
|
+
// Exit with error if any cleanups failed
|
|
352
|
+
if (!result.success) {
|
|
353
|
+
process.exit(EXIT_CODES.ERROR);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
main().catch((e) => {
|
|
358
|
+
console.error(`${LOG_PREFIX} ${e.message}`);
|
|
359
|
+
process.exit(EXIT_CODES.ERROR);
|
|
360
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Doctor Fix Operations (WU-1230)
|
|
3
|
+
*
|
|
4
|
+
* Provides fix dependencies for state:doctor --fix that use micro-worktree
|
|
5
|
+
* isolation for all tracked file changes. This ensures:
|
|
6
|
+
*
|
|
7
|
+
* 1. No direct file modifications on main branch
|
|
8
|
+
* 2. Removal of stale WU references from backlog.md and status.md
|
|
9
|
+
* 3. All changes pushed via merge, not direct file modification
|
|
10
|
+
*
|
|
11
|
+
* @see {@link ./state-doctor.ts} - Main CLI that uses these deps
|
|
12
|
+
* @see {@link @lumenflow/core/dist/micro-worktree.js} - Micro-worktree infrastructure
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
17
|
+
/**
|
|
18
|
+
* Operation name for micro-worktree isolation
|
|
19
|
+
*/
|
|
20
|
+
const OPERATION_NAME = 'state-doctor';
|
|
21
|
+
/**
|
|
22
|
+
* Log prefix for state:doctor output
|
|
23
|
+
*/
|
|
24
|
+
const LOG_PREFIX = '[state:doctor]';
|
|
25
|
+
/**
|
|
26
|
+
* Signals file path (relative to project root)
|
|
27
|
+
*/
|
|
28
|
+
const SIGNALS_FILE = '.lumenflow/memory/signals.jsonl';
|
|
29
|
+
/**
|
|
30
|
+
* WU events file path (relative to project root)
|
|
31
|
+
*/
|
|
32
|
+
const WU_EVENTS_FILE = '.lumenflow/state/wu-events.jsonl';
|
|
33
|
+
/**
|
|
34
|
+
* Backlog file path (relative to project root)
|
|
35
|
+
*/
|
|
36
|
+
const BACKLOG_FILE = 'docs/04-operations/tasks/backlog.md';
|
|
37
|
+
/**
|
|
38
|
+
* Status file path (relative to project root)
|
|
39
|
+
*/
|
|
40
|
+
const STATUS_FILE = 'docs/04-operations/tasks/status.md';
|
|
41
|
+
/**
|
|
42
|
+
* Remove lines containing a WU reference from markdown content
|
|
43
|
+
*
|
|
44
|
+
* @param content - Markdown file content
|
|
45
|
+
* @param wuId - WU ID to remove (e.g., 'WU-999')
|
|
46
|
+
* @returns Updated content with lines containing WU ID removed
|
|
47
|
+
*/
|
|
48
|
+
function removeWuReferences(content, wuId) {
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
const filtered = lines.filter((line) => !line.includes(wuId));
|
|
51
|
+
return filtered.join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read file content safely, returning empty string if file doesn't exist
|
|
55
|
+
*/
|
|
56
|
+
async function readFileSafe(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path from config
|
|
59
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create fix dependencies for state:doctor --fix that use micro-worktree isolation.
|
|
67
|
+
*
|
|
68
|
+
* WU-1230: All file modifications happen in a micro-worktree and are pushed
|
|
69
|
+
* to origin/main via merge. This prevents direct modifications to local main.
|
|
70
|
+
*
|
|
71
|
+
* @param baseDir - Project base directory
|
|
72
|
+
* @returns Partial StateDoctorDeps with fix operations
|
|
73
|
+
*/
|
|
74
|
+
export function createStateDoctorFixDeps(_baseDir) {
|
|
75
|
+
return {
|
|
76
|
+
/**
|
|
77
|
+
* Remove a signal by ID using micro-worktree isolation
|
|
78
|
+
*/
|
|
79
|
+
removeSignal: async (id) => {
|
|
80
|
+
await withMicroWorktree({
|
|
81
|
+
operation: OPERATION_NAME,
|
|
82
|
+
id: `remove-signal-${id}`,
|
|
83
|
+
logPrefix: LOG_PREFIX,
|
|
84
|
+
pushOnly: true,
|
|
85
|
+
execute: async ({ worktreePath }) => {
|
|
86
|
+
const signalsPath = path.join(worktreePath, SIGNALS_FILE);
|
|
87
|
+
const content = await readFileSafe(signalsPath);
|
|
88
|
+
if (!content) {
|
|
89
|
+
return { commitMessage: `fix: no signals file found`, files: [] };
|
|
90
|
+
}
|
|
91
|
+
const lines = content.split('\n').filter((line) => {
|
|
92
|
+
if (!line.trim())
|
|
93
|
+
return false;
|
|
94
|
+
try {
|
|
95
|
+
const signal = JSON.parse(line);
|
|
96
|
+
return signal.id !== id;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return true; // Keep malformed lines
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
|
|
103
|
+
await fs.writeFile(signalsPath, lines.join('\n') + '\n', 'utf-8');
|
|
104
|
+
return {
|
|
105
|
+
commitMessage: `fix(state-doctor): remove dangling signal ${id}`,
|
|
106
|
+
files: [SIGNALS_FILE],
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* Remove events for a WU and clean up stale references from backlog.md and status.md
|
|
113
|
+
* using micro-worktree isolation.
|
|
114
|
+
*
|
|
115
|
+
* WU-1230: Also removes references to the WU from backlog.md and status.md
|
|
116
|
+
* to prevent stale WU links.
|
|
117
|
+
*/
|
|
118
|
+
removeEvent: async (wuId) => {
|
|
119
|
+
await withMicroWorktree({
|
|
120
|
+
operation: OPERATION_NAME,
|
|
121
|
+
id: `remove-event-${wuId.toLowerCase()}`,
|
|
122
|
+
logPrefix: LOG_PREFIX,
|
|
123
|
+
pushOnly: true,
|
|
124
|
+
execute: async ({ worktreePath }) => {
|
|
125
|
+
const modifiedFiles = [];
|
|
126
|
+
// 1. Remove events for this WU
|
|
127
|
+
const eventsPath = path.join(worktreePath, WU_EVENTS_FILE);
|
|
128
|
+
const eventsContent = await readFileSafe(eventsPath);
|
|
129
|
+
if (eventsContent) {
|
|
130
|
+
const lines = eventsContent.split('\n').filter((line) => {
|
|
131
|
+
if (!line.trim())
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
const event = JSON.parse(line);
|
|
135
|
+
return event.wuId !== wuId;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return true; // Keep malformed lines
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
|
|
142
|
+
await fs.writeFile(eventsPath, lines.join('\n') + '\n', 'utf-8');
|
|
143
|
+
modifiedFiles.push(WU_EVENTS_FILE);
|
|
144
|
+
}
|
|
145
|
+
// 2. Remove stale WU references from backlog.md
|
|
146
|
+
const backlogPath = path.join(worktreePath, BACKLOG_FILE);
|
|
147
|
+
const backlogContent = await readFileSafe(backlogPath);
|
|
148
|
+
if (backlogContent && backlogContent.includes(wuId)) {
|
|
149
|
+
const updatedBacklog = removeWuReferences(backlogContent, wuId);
|
|
150
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
|
|
151
|
+
await fs.writeFile(backlogPath, updatedBacklog, 'utf-8');
|
|
152
|
+
modifiedFiles.push(BACKLOG_FILE);
|
|
153
|
+
}
|
|
154
|
+
// 3. Remove stale WU references from status.md
|
|
155
|
+
const statusPath = path.join(worktreePath, STATUS_FILE);
|
|
156
|
+
const statusContent = await readFileSafe(statusPath);
|
|
157
|
+
if (statusContent && statusContent.includes(wuId)) {
|
|
158
|
+
const updatedStatus = removeWuReferences(statusContent, wuId);
|
|
159
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
|
|
160
|
+
await fs.writeFile(statusPath, updatedStatus, 'utf-8');
|
|
161
|
+
modifiedFiles.push(STATUS_FILE);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
commitMessage: `fix(state-doctor): remove broken events and references for ${wuId}`,
|
|
165
|
+
files: modifiedFiles,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* Create a stamp for a WU using micro-worktree isolation
|
|
172
|
+
*/
|
|
173
|
+
createStamp: async (wuId, title) => {
|
|
174
|
+
await withMicroWorktree({
|
|
175
|
+
operation: OPERATION_NAME,
|
|
176
|
+
id: `create-stamp-${wuId.toLowerCase()}`,
|
|
177
|
+
logPrefix: LOG_PREFIX,
|
|
178
|
+
pushOnly: true,
|
|
179
|
+
execute: async ({ worktreePath }) => {
|
|
180
|
+
const stampsDir = path.join(worktreePath, '.lumenflow/stamps');
|
|
181
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
182
|
+
await fs.mkdir(stampsDir, { recursive: true });
|
|
183
|
+
// Create stamp file in micro-worktree
|
|
184
|
+
const stampPath = path.join(stampsDir, `${wuId}.done`);
|
|
185
|
+
const stampContent = `# ${wuId} Done\n\nTitle: ${title}\nCreated by: state:doctor --fix\nTimestamp: ${new Date().toISOString()}\n`;
|
|
186
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path
|
|
187
|
+
await fs.writeFile(stampPath, stampContent, 'utf-8');
|
|
188
|
+
return {
|
|
189
|
+
commitMessage: `fix(state-doctor): create missing stamp for ${wuId}`,
|
|
190
|
+
files: [`.lumenflow/stamps/${wuId}.done`],
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|