@lumenflow/cli 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/safe-git.test.js +191 -0
  10. package/dist/__tests__/state-doctor.test.js +274 -0
  11. package/dist/__tests__/wu-done.test.js +36 -0
  12. package/dist/__tests__/wu-edit.test.js +119 -0
  13. package/dist/__tests__/wu-prep.test.js +108 -0
  14. package/dist/agent-issues-query.js +4 -3
  15. package/dist/agent-log-issue.js +25 -4
  16. package/dist/backlog-prune.js +5 -4
  17. package/dist/cli-entry-point.js +11 -1
  18. package/dist/doctor.js +368 -0
  19. package/dist/flow-bottlenecks.js +6 -5
  20. package/dist/flow-report.js +4 -3
  21. package/dist/gates.js +356 -101
  22. package/dist/guard-locked.js +4 -3
  23. package/dist/guard-worktree-commit.js +4 -3
  24. package/dist/init.js +508 -86
  25. package/dist/initiative-add-wu.js +4 -3
  26. package/dist/initiative-bulk-assign-wus.js +8 -5
  27. package/dist/initiative-create.js +73 -37
  28. package/dist/initiative-edit.js +37 -21
  29. package/dist/initiative-list.js +4 -3
  30. package/dist/initiative-plan.js +337 -0
  31. package/dist/initiative-status.js +4 -3
  32. package/dist/lane-health.js +377 -0
  33. package/dist/lane-suggest.js +382 -0
  34. package/dist/mem-checkpoint.js +2 -2
  35. package/dist/mem-cleanup.js +2 -2
  36. package/dist/mem-context.js +306 -0
  37. package/dist/mem-create.js +2 -2
  38. package/dist/mem-delete.js +293 -0
  39. package/dist/mem-inbox.js +2 -2
  40. package/dist/mem-index.js +211 -0
  41. package/dist/mem-init.js +1 -1
  42. package/dist/mem-profile.js +207 -0
  43. package/dist/mem-promote.js +254 -0
  44. package/dist/mem-ready.js +2 -2
  45. package/dist/mem-signal.js +2 -2
  46. package/dist/mem-start.js +2 -2
  47. package/dist/mem-summarize.js +2 -2
  48. package/dist/mem-triage.js +2 -2
  49. package/dist/merge-block.js +222 -0
  50. package/dist/metrics-cli.js +7 -4
  51. package/dist/metrics-snapshot.js +4 -3
  52. package/dist/orchestrate-initiative.js +10 -4
  53. package/dist/orchestrate-monitor.js +379 -31
  54. package/dist/signal-cleanup.js +296 -0
  55. package/dist/spawn-list.js +6 -5
  56. package/dist/state-bootstrap.js +5 -4
  57. package/dist/state-cleanup.js +360 -0
  58. package/dist/state-doctor-fix.js +196 -0
  59. package/dist/state-doctor.js +501 -0
  60. package/dist/validate-agent-skills.js +4 -3
  61. package/dist/validate-agent-sync.js +4 -3
  62. package/dist/validate-backlog-sync.js +4 -3
  63. package/dist/validate-skills-spec.js +4 -3
  64. package/dist/validate.js +4 -3
  65. package/dist/wu-block.js +3 -3
  66. package/dist/wu-claim.js +208 -98
  67. package/dist/wu-cleanup.js +5 -4
  68. package/dist/wu-create.js +71 -46
  69. package/dist/wu-delete.js +88 -60
  70. package/dist/wu-deps.js +6 -5
  71. package/dist/wu-done-check.js +34 -0
  72. package/dist/wu-done.js +39 -12
  73. package/dist/wu-edit.js +63 -28
  74. package/dist/wu-infer-lane.js +7 -6
  75. package/dist/wu-preflight.js +23 -81
  76. package/dist/wu-prep.js +125 -0
  77. package/dist/wu-prune.js +4 -3
  78. package/dist/wu-recover.js +88 -22
  79. package/dist/wu-repair.js +7 -6
  80. package/dist/wu-spawn.js +226 -270
  81. package/dist/wu-status.js +4 -3
  82. package/dist/wu-unblock.js +5 -5
  83. package/dist/wu-unlock-lane.js +4 -3
  84. package/dist/wu-validate.js +5 -4
  85. package/package.json +16 -7
  86. package/templates/core/.lumenflow/constraints.md.template +192 -0
  87. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  88. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  89. package/templates/core/AGENTS.md.template +60 -0
  90. package/templates/core/LUMENFLOW.md.template +255 -0
  91. package/templates/core/UPGRADING.md.template +121 -0
  92. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  93. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  94. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  95. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  96. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  97. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  98. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  99. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  100. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  101. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  102. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  103. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  104. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  105. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  106. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  107. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  108. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  109. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  110. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  111. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  112. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  113. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  114. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  115. package/templates/vendors/cline/.clinerules.template +53 -0
  116. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  117. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  118. 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
+ }