@lumenflow/cli 2.7.0 → 2.8.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/README.md +120 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +27 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +0 -2
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +256 -13
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +8 -7
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file integrate.ts
|
|
3
|
+
* Integrate LumenFlow with Claude Code (WU-1367)
|
|
4
|
+
*
|
|
5
|
+
* This command generates enforcement hooks and updates Claude Code
|
|
6
|
+
* configuration based on .lumenflow.config.yaml settings.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm lumenflow:integrate --client claude-code
|
|
10
|
+
*/
|
|
11
|
+
// CLI tool - console output is intentional for user feedback
|
|
12
|
+
// fs operations use runtime-provided paths from LumenFlow configuration
|
|
13
|
+
// Object injection sink warnings are false positives for array indexing
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import * as yaml from 'yaml';
|
|
17
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core';
|
|
18
|
+
import { generateEnforcementHooks, generateEnforceWorktreeScript, generateRequireWuScript, generateWarnIncompleteScript, } from '../hooks/enforcement-generator.js';
|
|
19
|
+
/**
|
|
20
|
+
* CLI options for integrate command
|
|
21
|
+
*/
|
|
22
|
+
const INTEGRATE_OPTIONS = {
|
|
23
|
+
client: {
|
|
24
|
+
name: 'client',
|
|
25
|
+
flags: '--client <type>',
|
|
26
|
+
description: 'Client type to integrate (claude-code)',
|
|
27
|
+
},
|
|
28
|
+
force: WU_OPTIONS.force,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Parse command line options
|
|
32
|
+
*/
|
|
33
|
+
export function parseIntegrateOptions() {
|
|
34
|
+
const opts = createWUParser({
|
|
35
|
+
name: 'lumenflow-integrate',
|
|
36
|
+
description: 'Integrate LumenFlow enforcement with AI client tools',
|
|
37
|
+
options: Object.values(INTEGRATE_OPTIONS),
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
client: opts.client ?? 'claude-code',
|
|
41
|
+
force: opts.force ?? false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read existing Claude settings.json
|
|
46
|
+
*/
|
|
47
|
+
function readClaudeSettings(projectDir) {
|
|
48
|
+
const settingsPath = path.join(projectDir, '.claude', 'settings.json');
|
|
49
|
+
if (!fs.existsSync(settingsPath)) {
|
|
50
|
+
return {
|
|
51
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
52
|
+
permissions: {
|
|
53
|
+
allow: ['Bash', 'Read', 'Write', 'Edit', 'WebFetch', 'WebSearch', 'Skill'],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {
|
|
63
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merge generated hooks into existing settings
|
|
69
|
+
*/
|
|
70
|
+
// Complexity is acceptable for hook merging logic - alternative would over-abstract
|
|
71
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
72
|
+
function mergeHooksIntoSettings(existing, generated) {
|
|
73
|
+
const result = { ...existing };
|
|
74
|
+
if (!result.hooks) {
|
|
75
|
+
result.hooks = {};
|
|
76
|
+
}
|
|
77
|
+
// Merge PreToolUse hooks
|
|
78
|
+
if (generated.preToolUse) {
|
|
79
|
+
if (!result.hooks.PreToolUse) {
|
|
80
|
+
result.hooks.PreToolUse = [];
|
|
81
|
+
}
|
|
82
|
+
for (const newHook of generated.preToolUse) {
|
|
83
|
+
const existingIndex = result.hooks.PreToolUse.findIndex((h) => h.matcher === newHook.matcher);
|
|
84
|
+
if (existingIndex >= 0) {
|
|
85
|
+
const existing = result.hooks.PreToolUse[existingIndex];
|
|
86
|
+
for (const hook of newHook.hooks) {
|
|
87
|
+
const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
|
|
88
|
+
if (!isDuplicate) {
|
|
89
|
+
existing.hooks.push(hook);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result.hooks.PreToolUse.push(newHook);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Merge Stop hooks
|
|
99
|
+
if (generated.stop) {
|
|
100
|
+
if (!result.hooks.Stop) {
|
|
101
|
+
result.hooks.Stop = [];
|
|
102
|
+
}
|
|
103
|
+
for (const newHook of generated.stop) {
|
|
104
|
+
const existingIndex = result.hooks.Stop.findIndex((h) => h.matcher === newHook.matcher);
|
|
105
|
+
if (existingIndex >= 0) {
|
|
106
|
+
const existing = result.hooks.Stop[existingIndex];
|
|
107
|
+
for (const hook of newHook.hooks) {
|
|
108
|
+
const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
|
|
109
|
+
if (!isDuplicate) {
|
|
110
|
+
existing.hooks.push(hook);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
result.hooks.Stop.push(newHook);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Integrate Claude Code with LumenFlow enforcement hooks.
|
|
123
|
+
*
|
|
124
|
+
* This function:
|
|
125
|
+
* 1. Creates .claude/hooks directory if needed
|
|
126
|
+
* 2. Generates enforcement hook scripts
|
|
127
|
+
* 3. Updates .claude/settings.json with hook configuration
|
|
128
|
+
*
|
|
129
|
+
* @param projectDir - Project directory
|
|
130
|
+
* @param config - Client configuration with enforcement settings
|
|
131
|
+
*/
|
|
132
|
+
export async function integrateClaudeCode(projectDir, config) {
|
|
133
|
+
const enforcement = config.enforcement;
|
|
134
|
+
// Skip if enforcement not enabled
|
|
135
|
+
if (!enforcement?.hooks) {
|
|
136
|
+
console.log('[integrate] Enforcement hooks not enabled, skipping');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
140
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
141
|
+
// Create directories
|
|
142
|
+
if (!fs.existsSync(hooksDir)) {
|
|
143
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
144
|
+
console.log('[integrate] Created .claude/hooks directory');
|
|
145
|
+
}
|
|
146
|
+
// Generate hooks based on config
|
|
147
|
+
const generatedHooks = generateEnforcementHooks({
|
|
148
|
+
block_outside_worktree: enforcement.block_outside_worktree ?? false,
|
|
149
|
+
require_wu_for_edits: enforcement.require_wu_for_edits ?? false,
|
|
150
|
+
warn_on_stop_without_wu_done: enforcement.warn_on_stop_without_wu_done ?? false,
|
|
151
|
+
});
|
|
152
|
+
// Write hook scripts
|
|
153
|
+
if (enforcement.block_outside_worktree) {
|
|
154
|
+
const scriptPath = path.join(hooksDir, 'enforce-worktree.sh');
|
|
155
|
+
fs.writeFileSync(scriptPath, generateEnforceWorktreeScript(), { mode: 0o755 });
|
|
156
|
+
console.log('[integrate] Generated enforce-worktree.sh');
|
|
157
|
+
}
|
|
158
|
+
if (enforcement.require_wu_for_edits) {
|
|
159
|
+
const scriptPath = path.join(hooksDir, 'require-wu.sh');
|
|
160
|
+
fs.writeFileSync(scriptPath, generateRequireWuScript(), { mode: 0o755 });
|
|
161
|
+
console.log('[integrate] Generated require-wu.sh');
|
|
162
|
+
}
|
|
163
|
+
if (enforcement.warn_on_stop_without_wu_done) {
|
|
164
|
+
const scriptPath = path.join(hooksDir, 'warn-incomplete.sh');
|
|
165
|
+
fs.writeFileSync(scriptPath, generateWarnIncompleteScript(), { mode: 0o755 });
|
|
166
|
+
console.log('[integrate] Generated warn-incomplete.sh');
|
|
167
|
+
}
|
|
168
|
+
// Update settings.json
|
|
169
|
+
const existingSettings = readClaudeSettings(projectDir);
|
|
170
|
+
const updatedSettings = mergeHooksIntoSettings(existingSettings, generatedHooks);
|
|
171
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
172
|
+
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2) + '\n', 'utf-8');
|
|
173
|
+
console.log('[integrate] Updated .claude/settings.json');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read enforcement config from .lumenflow.config.yaml
|
|
177
|
+
*/
|
|
178
|
+
function readEnforcementConfig(projectDir) {
|
|
179
|
+
const configPath = path.join(projectDir, '.lumenflow.config.yaml');
|
|
180
|
+
if (!fs.existsSync(configPath)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
185
|
+
const config = yaml.parse(content);
|
|
186
|
+
return config?.agents?.clients?.['claude-code']?.enforcement ?? null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Main entry point for integrate command
|
|
194
|
+
*/
|
|
195
|
+
export async function main() {
|
|
196
|
+
const opts = parseIntegrateOptions();
|
|
197
|
+
if (opts.client !== 'claude-code') {
|
|
198
|
+
console.error(`[integrate] Unsupported client: ${opts.client}`);
|
|
199
|
+
console.error('[integrate] Currently only "claude-code" is supported');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const projectDir = process.cwd();
|
|
203
|
+
// Read enforcement config from .lumenflow.config.yaml
|
|
204
|
+
const enforcement = readEnforcementConfig(projectDir);
|
|
205
|
+
if (!enforcement) {
|
|
206
|
+
console.log('[integrate] No enforcement config found in .lumenflow.config.yaml');
|
|
207
|
+
console.log('[integrate] Add this to your config to enable enforcement hooks:');
|
|
208
|
+
console.log(`
|
|
209
|
+
agents:
|
|
210
|
+
clients:
|
|
211
|
+
claude-code:
|
|
212
|
+
enforcement:
|
|
213
|
+
hooks: true
|
|
214
|
+
block_outside_worktree: true
|
|
215
|
+
require_wu_for_edits: true
|
|
216
|
+
warn_on_stop_without_wu_done: true
|
|
217
|
+
`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
await integrateClaudeCode(projectDir, { enforcement });
|
|
221
|
+
console.log('[integrate] Claude Code integration complete');
|
|
222
|
+
}
|
|
223
|
+
// Run if executed directly
|
|
224
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
225
|
+
main().catch((err) => {
|
|
226
|
+
console.error('[integrate] Error:', err.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
|
229
|
+
}
|
package/dist/docs-sync.js
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* LumenFlow docs:sync command for syncing agent docs to existing projects (WU-1083)
|
|
4
4
|
* WU-1085: Added createWUParser for proper --help support
|
|
5
5
|
* WU-1124: Refactored to read templates from bundled files (INIT-004 Phase 2)
|
|
6
|
+
* WU-1362: Added branch guard to check branch before writing tracked files
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from 'node:fs';
|
|
8
9
|
import * as path from 'node:path';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core';
|
|
12
|
+
// WU-1362: Import worktree guard utilities for branch checking
|
|
13
|
+
import { isMainBranch, isInWorktree } from '@lumenflow/core/dist/core/worktree-guard.js';
|
|
11
14
|
/**
|
|
12
15
|
* WU-1085: CLI option definitions for docs-sync command
|
|
13
16
|
*/
|
|
@@ -182,9 +185,45 @@ export async function syncSkills(targetDir, options) {
|
|
|
182
185
|
}
|
|
183
186
|
return result;
|
|
184
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* WU-1362: Check branch guard before writing tracked files
|
|
190
|
+
*
|
|
191
|
+
* Warns (but does not block) if:
|
|
192
|
+
* - On main branch AND
|
|
193
|
+
* - Not in a worktree directory AND
|
|
194
|
+
* - Git repository exists (has .git)
|
|
195
|
+
*
|
|
196
|
+
* @param targetDir - Directory where files will be written
|
|
197
|
+
* @returns Array of warning messages
|
|
198
|
+
*/
|
|
199
|
+
async function checkBranchGuard(targetDir) {
|
|
200
|
+
const warnings = [];
|
|
201
|
+
// Only check if target is a git repository
|
|
202
|
+
const gitDir = path.join(targetDir, '.git');
|
|
203
|
+
if (!fs.existsSync(gitDir)) {
|
|
204
|
+
return warnings;
|
|
205
|
+
}
|
|
206
|
+
// Check if we're in a worktree (always allow)
|
|
207
|
+
if (isInWorktree({ cwd: targetDir })) {
|
|
208
|
+
return warnings;
|
|
209
|
+
}
|
|
210
|
+
// Check if on main branch
|
|
211
|
+
try {
|
|
212
|
+
const onMain = await isMainBranch();
|
|
213
|
+
if (onMain) {
|
|
214
|
+
warnings.push('Running docs:sync on main branch in main checkout. ' +
|
|
215
|
+
'Consider using a worktree for changes to tracked files.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Git error - silently allow
|
|
220
|
+
}
|
|
221
|
+
return warnings;
|
|
222
|
+
}
|
|
185
223
|
/**
|
|
186
224
|
* CLI entry point for docs:sync command
|
|
187
225
|
* WU-1085: Updated to use parseDocsSyncOptions for proper --help support
|
|
226
|
+
* WU-1362: Added branch guard check
|
|
188
227
|
*/
|
|
189
228
|
export async function main() {
|
|
190
229
|
const opts = parseDocsSyncOptions();
|
|
@@ -192,10 +231,13 @@ export async function main() {
|
|
|
192
231
|
console.log('[lumenflow docs:sync] Syncing agent documentation...');
|
|
193
232
|
console.log(` Vendor: ${opts.vendor}`);
|
|
194
233
|
console.log(` Force: ${opts.force}`);
|
|
234
|
+
// WU-1362: Check branch guard before writing files
|
|
235
|
+
const branchWarnings = await checkBranchGuard(targetDir);
|
|
195
236
|
const docsResult = await syncAgentDocs(targetDir, { force: opts.force });
|
|
196
237
|
const skillsResult = await syncSkills(targetDir, { force: opts.force, vendor: opts.vendor });
|
|
197
238
|
const created = [...docsResult.created, ...skillsResult.created];
|
|
198
239
|
const skipped = [...docsResult.skipped, ...skillsResult.skipped];
|
|
240
|
+
const warnings = [...branchWarnings];
|
|
199
241
|
if (created.length > 0) {
|
|
200
242
|
console.log('\nCreated:');
|
|
201
243
|
created.forEach((f) => console.log(` + ${f}`));
|
|
@@ -204,6 +246,10 @@ export async function main() {
|
|
|
204
246
|
console.log('\nSkipped (already exists, use --force to overwrite):');
|
|
205
247
|
skipped.forEach((f) => console.log(` - ${f}`));
|
|
206
248
|
}
|
|
249
|
+
if (warnings.length > 0) {
|
|
250
|
+
console.log('\nWarnings:');
|
|
251
|
+
warnings.forEach((w) => console.log(` ! ${w}`));
|
|
252
|
+
}
|
|
207
253
|
console.log('\n[lumenflow docs:sync] Done!');
|
|
208
254
|
}
|
|
209
255
|
// CLI entry point (WU-1071 pattern: import.meta.main)
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console -- CLI command uses console for status output */
|
|
2
1
|
/**
|
|
3
2
|
* @file doctor.ts
|
|
4
3
|
* LumenFlow health check command (WU-1177)
|
|
@@ -167,7 +166,6 @@ function getCommandVersion(command, args) {
|
|
|
167
166
|
* Parse semver version string to compare
|
|
168
167
|
*/
|
|
169
168
|
function parseVersion(versionStr) {
|
|
170
|
-
// eslint-disable-next-line sonarjs/slow-regex, sonarjs/prefer-regexp-exec -- Simple semver extraction, no backtracking risk
|
|
171
169
|
const match = versionStr.match(/(\d+)\.(\d+)\.?(\d+)?/);
|
|
172
170
|
if (!match) {
|
|
173
171
|
return [0, 0, 0];
|
package/dist/gates.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console -- Gates runner uses console for status output; refactoring to logger is tracked for future work */
|
|
3
2
|
/**
|
|
4
3
|
* Quality Gates Runner
|
|
5
4
|
*
|
|
@@ -469,7 +468,6 @@ export function formatFormatCheckGuidance(files) {
|
|
|
469
468
|
function collectPrettierListDifferent(cwd, files = []) {
|
|
470
469
|
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
471
470
|
const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
|
|
472
|
-
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: executes trusted pnpm prettier command
|
|
473
471
|
const result = spawnSync(cmd, [], {
|
|
474
472
|
shell: true,
|
|
475
473
|
cwd,
|
|
@@ -534,7 +532,6 @@ function run(cmd, { agentLog } = {}) {
|
|
|
534
532
|
if (!agentLog) {
|
|
535
533
|
console.log(`\n> ${cmd}\n`);
|
|
536
534
|
try {
|
|
537
|
-
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
538
535
|
execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
|
|
539
536
|
return { ok: true, duration: Date.now() - start };
|
|
540
537
|
}
|
|
@@ -543,7 +540,6 @@ function run(cmd, { agentLog } = {}) {
|
|
|
543
540
|
}
|
|
544
541
|
}
|
|
545
542
|
writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
|
|
546
|
-
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
547
543
|
const result = spawnSync(cmd, [], {
|
|
548
544
|
shell: true,
|
|
549
545
|
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
@@ -704,13 +700,11 @@ async function runFormatCheckGate({ agentLog, useAgentMode }) {
|
|
|
704
700
|
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
705
701
|
}
|
|
706
702
|
if (plan.mode === 'full') {
|
|
707
|
-
/* eslint-disable sonarjs/no-nested-conditional -- Pre-existing: simple reason mapping, readable as-is */
|
|
708
703
|
const reason = plan.reason === 'prettier-config'
|
|
709
704
|
? ' (prettier config changed)'
|
|
710
705
|
: plan.reason === 'file-list-error'
|
|
711
706
|
? ' (file list unavailable)'
|
|
712
707
|
: '';
|
|
713
|
-
/* eslint-enable sonarjs/no-nested-conditional */
|
|
714
708
|
logLine(`📋 Running full format check${reason}`);
|
|
715
709
|
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
|
|
716
710
|
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
@@ -1408,7 +1402,6 @@ async function executeGates(opts) {
|
|
|
1408
1402
|
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
1409
1403
|
// path but import.meta.url resolves to the real path - they never match
|
|
1410
1404
|
if (import.meta.main) {
|
|
1411
|
-
// eslint-disable-next-line sonarjs/deprecation -- Pre-existing: parseGatesArgs kept for backwards compatibility
|
|
1412
1405
|
const opts = parseGatesArgs();
|
|
1413
1406
|
executeGates({ ...opts, argv: process.argv.slice(2) })
|
|
1414
1407
|
.then((ok) => {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file enforcement-checks.ts
|
|
3
|
+
* Runtime enforcement checks for LumenFlow workflow compliance (WU-1367)
|
|
4
|
+
*
|
|
5
|
+
* These functions can be used by hooks to validate operations.
|
|
6
|
+
* All checks implement graceful degradation: if state cannot be
|
|
7
|
+
* determined, operations are allowed.
|
|
8
|
+
*/
|
|
9
|
+
// Note: fs operations use runtime-provided paths from LumenFlow configuration
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
/**
|
|
13
|
+
* Check if a Write/Edit operation should be allowed based on worktree enforcement.
|
|
14
|
+
*
|
|
15
|
+
* Implements graceful degradation: if LumenFlow state cannot be determined,
|
|
16
|
+
* the operation is allowed to prevent blocking legitimate work.
|
|
17
|
+
*
|
|
18
|
+
* @param input - Tool input with file_path and tool_name
|
|
19
|
+
* @param projectDir - Project directory (defaults to CLAUDE_PROJECT_DIR)
|
|
20
|
+
* @returns Check result with allowed status and reason
|
|
21
|
+
*/
|
|
22
|
+
export async function checkWorktreeEnforcement(input, projectDir) {
|
|
23
|
+
const mainRepoPath = projectDir ?? process.env.CLAUDE_PROJECT_DIR;
|
|
24
|
+
// Graceful degradation: no project dir
|
|
25
|
+
if (!mainRepoPath) {
|
|
26
|
+
return {
|
|
27
|
+
allowed: true,
|
|
28
|
+
reason: 'graceful: CLAUDE_PROJECT_DIR not set',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const lumenflowDir = path.join(mainRepoPath, '.lumenflow');
|
|
32
|
+
const worktreesDir = path.join(mainRepoPath, 'worktrees');
|
|
33
|
+
// Graceful degradation: LumenFlow not configured
|
|
34
|
+
if (!fs.existsSync(lumenflowDir)) {
|
|
35
|
+
return {
|
|
36
|
+
allowed: true,
|
|
37
|
+
reason: 'graceful: LumenFlow not configured',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// No worktrees = no enforcement needed
|
|
41
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
42
|
+
return {
|
|
43
|
+
allowed: true,
|
|
44
|
+
reason: 'no worktrees exist',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Check for active worktrees
|
|
48
|
+
let worktreeCount = 0;
|
|
49
|
+
try {
|
|
50
|
+
const entries = fs.readdirSync(worktreesDir);
|
|
51
|
+
worktreeCount = entries.filter((e) => {
|
|
52
|
+
const stat = fs.statSync(path.join(worktreesDir, e));
|
|
53
|
+
return stat.isDirectory();
|
|
54
|
+
}).length;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {
|
|
58
|
+
allowed: true,
|
|
59
|
+
reason: 'graceful: cannot read worktrees directory',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (worktreeCount === 0) {
|
|
63
|
+
return {
|
|
64
|
+
allowed: true,
|
|
65
|
+
reason: 'no active worktrees',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Resolve the file path
|
|
69
|
+
let resolvedPath;
|
|
70
|
+
try {
|
|
71
|
+
resolvedPath = path.resolve(input.file_path);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return {
|
|
75
|
+
allowed: true,
|
|
76
|
+
reason: 'graceful: cannot resolve file path',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Allow if path is inside a worktree
|
|
80
|
+
if (resolvedPath.startsWith(worktreesDir + path.sep)) {
|
|
81
|
+
return {
|
|
82
|
+
allowed: true,
|
|
83
|
+
reason: 'path is inside worktree',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Block if path is in main repo while worktrees exist
|
|
87
|
+
if (resolvedPath.startsWith(mainRepoPath + path.sep) || resolvedPath === mainRepoPath) {
|
|
88
|
+
const activeWorktrees = fs
|
|
89
|
+
.readdirSync(worktreesDir)
|
|
90
|
+
.filter((e) => fs.statSync(path.join(worktreesDir, e)).isDirectory())
|
|
91
|
+
.slice(0, 5)
|
|
92
|
+
.join(', ');
|
|
93
|
+
return {
|
|
94
|
+
allowed: false,
|
|
95
|
+
reason: `cannot write to main repo while worktrees exist (${activeWorktrees})`,
|
|
96
|
+
suggestion: 'cd to your worktree: cd worktrees/<lane>-wu-<id>/',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Path is outside repo entirely - allow
|
|
100
|
+
return {
|
|
101
|
+
allowed: true,
|
|
102
|
+
reason: 'path is outside repository',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a Write/Edit operation should be allowed based on WU requirement.
|
|
107
|
+
*
|
|
108
|
+
* @param input - Tool input with file_path and tool_name
|
|
109
|
+
* @param projectDir - Project directory
|
|
110
|
+
* @returns Check result with allowed status and reason
|
|
111
|
+
*/
|
|
112
|
+
export async function checkWuRequirement(input, projectDir) {
|
|
113
|
+
const mainRepoPath = projectDir ?? process.env.CLAUDE_PROJECT_DIR;
|
|
114
|
+
if (!mainRepoPath) {
|
|
115
|
+
return {
|
|
116
|
+
allowed: true,
|
|
117
|
+
reason: 'graceful: CLAUDE_PROJECT_DIR not set',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const lumenflowDir = path.join(mainRepoPath, '.lumenflow');
|
|
121
|
+
const worktreesDir = path.join(mainRepoPath, 'worktrees');
|
|
122
|
+
const stateFile = path.join(lumenflowDir, 'state', 'wu-events.jsonl');
|
|
123
|
+
// Graceful degradation: LumenFlow not configured
|
|
124
|
+
if (!fs.existsSync(lumenflowDir)) {
|
|
125
|
+
return {
|
|
126
|
+
allowed: true,
|
|
127
|
+
reason: 'graceful: LumenFlow not configured',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Check for active worktrees (indicates claimed WU)
|
|
131
|
+
if (fs.existsSync(worktreesDir)) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = fs.readdirSync(worktreesDir);
|
|
134
|
+
const worktreeCount = entries.filter((e) => {
|
|
135
|
+
const stat = fs.statSync(path.join(worktreesDir, e));
|
|
136
|
+
return stat.isDirectory();
|
|
137
|
+
}).length;
|
|
138
|
+
if (worktreeCount > 0) {
|
|
139
|
+
return {
|
|
140
|
+
allowed: true,
|
|
141
|
+
reason: 'has active worktree (claimed WU)',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Continue to state file check
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Check state file for in_progress WUs
|
|
150
|
+
if (fs.existsSync(stateFile)) {
|
|
151
|
+
try {
|
|
152
|
+
const content = fs.readFileSync(stateFile, 'utf-8');
|
|
153
|
+
if (content.includes('"status":"in_progress"')) {
|
|
154
|
+
return {
|
|
155
|
+
allowed: true,
|
|
156
|
+
reason: 'has in_progress WU in state',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return {
|
|
162
|
+
allowed: true,
|
|
163
|
+
reason: 'graceful: cannot read state file',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// No claimed WU found
|
|
168
|
+
return {
|
|
169
|
+
allowed: false,
|
|
170
|
+
reason: 'no WU claimed',
|
|
171
|
+
suggestion: 'pnpm wu:claim --id WU-XXXX --lane <Lane>',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check if the current working directory is inside a worktree.
|
|
176
|
+
*
|
|
177
|
+
* @param cwd - Current working directory
|
|
178
|
+
* @param projectDir - Project directory
|
|
179
|
+
* @returns True if cwd is inside a worktree
|
|
180
|
+
*/
|
|
181
|
+
export function isInsideWorktree(cwd, projectDir) {
|
|
182
|
+
const worktreesDir = path.join(projectDir, 'worktrees');
|
|
183
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const resolvedCwd = path.resolve(cwd);
|
|
187
|
+
return resolvedCwd.startsWith(worktreesDir + path.sep);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get list of active worktrees.
|
|
191
|
+
*
|
|
192
|
+
* @param projectDir - Project directory
|
|
193
|
+
* @returns Array of worktree names
|
|
194
|
+
*/
|
|
195
|
+
export function getActiveWorktrees(projectDir) {
|
|
196
|
+
const worktreesDir = path.join(projectDir, 'worktrees');
|
|
197
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return fs.readdirSync(worktreesDir).filter((e) => {
|
|
202
|
+
const stat = fs.statSync(path.join(worktreesDir, e));
|
|
203
|
+
return stat.isDirectory();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|