@lumenflow/cli 2.7.0 → 2.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/README.md +121 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -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 +249 -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/commands.js +171 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +479 -10
- 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 +502 -17
- 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 +9 -7
package/dist/sync-templates.js
CHANGED
|
@@ -2,24 +2,29 @@
|
|
|
2
2
|
* @file sync-templates.ts
|
|
3
3
|
* Sync internal docs to CLI templates for release-cycle maintenance (WU-1123)
|
|
4
4
|
*
|
|
5
|
+
* WU-1368: Fixed two bugs:
|
|
6
|
+
* 1. --check-drift flag now is truly read-only (compares without writing)
|
|
7
|
+
* 2. sync:templates uses micro-worktree isolation for safe atomic commits
|
|
8
|
+
*
|
|
5
9
|
* This script syncs source docs from the hellmai/os repo to the templates
|
|
6
10
|
* directory, applying template variable substitutions:
|
|
7
11
|
* - Onboarding docs -> templates/core/ai/onboarding/
|
|
8
12
|
* - Claude skills -> templates/vendors/claude/.claude/skills/
|
|
9
13
|
* - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
|
|
10
14
|
*/
|
|
11
|
-
/* eslint-disable no-console -- CLI tool requires console output */
|
|
12
|
-
/* eslint-disable security/detect-non-literal-fs-filename -- CLI tool syncs templates from known paths */
|
|
13
|
-
/* eslint-disable security/detect-non-literal-regexp -- Dynamic date pattern for template substitution */
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
16
|
import * as path from 'node:path';
|
|
16
|
-
import { createWUParser } from '@lumenflow/core';
|
|
17
|
+
import { createWUParser, withMicroWorktree } from '@lumenflow/core';
|
|
17
18
|
// Directory name constants to avoid duplicate strings
|
|
18
19
|
const LUMENFLOW_DIR = '.lumenflow';
|
|
19
20
|
const CLAUDE_DIR = '.claude';
|
|
20
21
|
const SKILLS_DIR = 'skills';
|
|
21
22
|
// Template variable patterns
|
|
22
23
|
const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
|
|
24
|
+
// Log prefix for console output
|
|
25
|
+
const LOG_PREFIX = '[sync-templates]';
|
|
26
|
+
// Micro-worktree operation name
|
|
27
|
+
const OPERATION_NAME = 'sync-templates';
|
|
23
28
|
/**
|
|
24
29
|
* CLI option definitions for sync-templates command
|
|
25
30
|
*/
|
|
@@ -212,7 +217,6 @@ function checkFileDrift(sourcePath, templatePath, projectRoot) {
|
|
|
212
217
|
* to detect if templates have drifted out of sync. Used by CI to warn
|
|
213
218
|
* when templates need to be re-synced.
|
|
214
219
|
*/
|
|
215
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- Multi-category drift check requires nested iteration
|
|
216
220
|
export async function checkTemplateDrift(projectRoot) {
|
|
217
221
|
const driftingFiles = [];
|
|
218
222
|
const checkedFiles = [];
|
|
@@ -277,40 +281,145 @@ export async function checkTemplateDrift(projectRoot) {
|
|
|
277
281
|
};
|
|
278
282
|
}
|
|
279
283
|
/**
|
|
280
|
-
*
|
|
284
|
+
* Sync a single file to templates within a worktree path
|
|
285
|
+
*
|
|
286
|
+
* WU-1368: Internal helper for micro-worktree sync operations.
|
|
287
|
+
* Writes to worktreePath instead of projectRoot for isolation.
|
|
281
288
|
*/
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (opts.checkDrift) {
|
|
288
|
-
console.log('[sync-templates] Checking for template drift...');
|
|
289
|
-
const drift = await checkTemplateDrift(projectRoot);
|
|
290
|
-
if (opts.verbose) {
|
|
291
|
-
console.log(` Checked ${drift.checkedFiles.length} files`);
|
|
289
|
+
function syncFileToWorktree(sourcePath, targetPath, projectRoot, result) {
|
|
290
|
+
try {
|
|
291
|
+
if (!fs.existsSync(sourcePath)) {
|
|
292
|
+
result.errors.push(`Source not found: ${sourcePath}`);
|
|
293
|
+
return;
|
|
292
294
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
295
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
296
|
+
const templateContent = convertToTemplate(content, projectRoot);
|
|
297
|
+
ensureDir(path.dirname(targetPath));
|
|
298
|
+
fs.writeFileSync(targetPath, templateContent);
|
|
299
|
+
// Store relative path from project root (not worktree path)
|
|
300
|
+
const relPath = targetPath.includes('templates/')
|
|
301
|
+
? targetPath.substring(targetPath.indexOf('packages/'))
|
|
302
|
+
: path.basename(targetPath);
|
|
303
|
+
result.synced.push(relPath);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
result.errors.push(`Error syncing ${sourcePath}: ${error.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Sync templates using micro-worktree isolation (WU-1368)
|
|
311
|
+
*
|
|
312
|
+
* This function uses the micro-worktree pattern to atomically sync templates:
|
|
313
|
+
* 1. Create temp branch in micro-worktree
|
|
314
|
+
* 2. Sync all templates to micro-worktree
|
|
315
|
+
* 3. Commit and push atomically
|
|
316
|
+
* 4. Cleanup
|
|
317
|
+
*
|
|
318
|
+
* Benefits:
|
|
319
|
+
* - Never modifies main checkout directly
|
|
320
|
+
* - Atomic commit with all template changes
|
|
321
|
+
* - Race-safe with other operations
|
|
322
|
+
*
|
|
323
|
+
* @param {string} projectRoot - Project root directory (source for templates)
|
|
324
|
+
* @returns {Promise<SyncSummary>} Summary of synced files
|
|
325
|
+
*/
|
|
326
|
+
export async function syncTemplatesWithWorktree(projectRoot) {
|
|
327
|
+
// Generate unique operation ID using timestamp
|
|
328
|
+
const operationId = `templates-${Date.now()}`;
|
|
329
|
+
console.log(`${LOG_PREFIX} Using micro-worktree isolation for atomic sync...`);
|
|
330
|
+
// Set env var for pre-push hook
|
|
331
|
+
const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
|
|
332
|
+
process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
|
|
333
|
+
try {
|
|
334
|
+
let syncResult = {
|
|
335
|
+
onboarding: { synced: [], errors: [] },
|
|
336
|
+
skills: { synced: [], errors: [] },
|
|
337
|
+
core: { synced: [], errors: [] },
|
|
338
|
+
};
|
|
339
|
+
await withMicroWorktree({
|
|
340
|
+
operation: OPERATION_NAME,
|
|
341
|
+
id: operationId,
|
|
342
|
+
logPrefix: LOG_PREFIX,
|
|
343
|
+
execute: async ({ worktreePath }) => {
|
|
344
|
+
const templatesDir = path.join(worktreePath, 'packages', '@lumenflow', 'cli', 'templates');
|
|
345
|
+
// Sync core docs
|
|
346
|
+
const coreResult = { synced: [], errors: [] };
|
|
347
|
+
const lumenflowSource = path.join(projectRoot, 'LUMENFLOW.md');
|
|
348
|
+
const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
|
|
349
|
+
syncFileToWorktree(lumenflowSource, lumenflowTarget, projectRoot, coreResult);
|
|
350
|
+
const constraintsSource = path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md');
|
|
351
|
+
const constraintsTarget = path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template');
|
|
352
|
+
syncFileToWorktree(constraintsSource, constraintsTarget, projectRoot, coreResult);
|
|
353
|
+
// Sync onboarding docs
|
|
354
|
+
const onboardingResult = { synced: [], errors: [] };
|
|
355
|
+
const onboardingSourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
356
|
+
const onboardingTargetDir = path.join(templatesDir, 'core', 'ai', 'onboarding');
|
|
357
|
+
if (fs.existsSync(onboardingSourceDir)) {
|
|
358
|
+
const files = fs.readdirSync(onboardingSourceDir).filter((f) => f.endsWith('.md'));
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
const sourcePath = path.join(onboardingSourceDir, file);
|
|
361
|
+
const targetPath = path.join(onboardingTargetDir, `${file}.template`);
|
|
362
|
+
syncFileToWorktree(sourcePath, targetPath, projectRoot, onboardingResult);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
onboardingResult.errors.push(`Onboarding source directory not found: ${onboardingSourceDir}`);
|
|
367
|
+
}
|
|
368
|
+
// Sync skills
|
|
369
|
+
const skillsResult = { synced: [], errors: [] };
|
|
370
|
+
const skillsSourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
|
|
371
|
+
const skillsTargetDir = path.join(templatesDir, 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
|
|
372
|
+
if (fs.existsSync(skillsSourceDir)) {
|
|
373
|
+
const skillDirs = fs
|
|
374
|
+
.readdirSync(skillsSourceDir, { withFileTypes: true })
|
|
375
|
+
.filter((d) => d.isDirectory())
|
|
376
|
+
.map((d) => d.name);
|
|
377
|
+
for (const skillName of skillDirs) {
|
|
378
|
+
const skillFile = path.join(skillsSourceDir, skillName, 'SKILL.md');
|
|
379
|
+
if (fs.existsSync(skillFile)) {
|
|
380
|
+
const targetPath = path.join(skillsTargetDir, skillName, 'SKILL.md.template');
|
|
381
|
+
syncFileToWorktree(skillFile, targetPath, projectRoot, skillsResult);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
skillsResult.errors.push(`Skills source directory not found: ${skillsSourceDir}`);
|
|
387
|
+
}
|
|
388
|
+
syncResult = {
|
|
389
|
+
onboarding: onboardingResult,
|
|
390
|
+
skills: skillsResult,
|
|
391
|
+
core: coreResult,
|
|
392
|
+
};
|
|
393
|
+
// Collect all synced files for commit
|
|
394
|
+
const allSyncedFiles = [
|
|
395
|
+
...coreResult.synced,
|
|
396
|
+
...onboardingResult.synced,
|
|
397
|
+
...skillsResult.synced,
|
|
398
|
+
];
|
|
399
|
+
const totalSynced = allSyncedFiles.length;
|
|
400
|
+
const commitMessage = `chore(sync:templates): sync ${totalSynced} template files`;
|
|
401
|
+
return {
|
|
402
|
+
commitMessage,
|
|
403
|
+
files: allSyncedFiles,
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
return syncResult;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
// Restore env var
|
|
411
|
+
if (previousWuTool === undefined) {
|
|
412
|
+
delete process.env.LUMENFLOW_WU_TOOL;
|
|
301
413
|
}
|
|
302
414
|
else {
|
|
303
|
-
|
|
415
|
+
process.env.LUMENFLOW_WU_TOOL = previousWuTool;
|
|
304
416
|
}
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
// Sync mode: update templates from source
|
|
308
|
-
console.log('[sync-templates] Syncing internal docs to CLI templates...');
|
|
309
|
-
if (opts.dryRun) {
|
|
310
|
-
console.log(' (dry-run mode - no files will be written)');
|
|
311
417
|
}
|
|
312
|
-
|
|
313
|
-
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Print sync results summary
|
|
421
|
+
*/
|
|
422
|
+
function printSyncResults(result) {
|
|
314
423
|
const sections = [
|
|
315
424
|
{ name: 'Onboarding docs', data: result.onboarding },
|
|
316
425
|
{ name: 'Claude skills', data: result.skills },
|
|
@@ -331,7 +440,52 @@ export async function main() {
|
|
|
331
440
|
}
|
|
332
441
|
}
|
|
333
442
|
}
|
|
334
|
-
|
|
443
|
+
return { totalSynced, totalErrors };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* CLI entry point
|
|
447
|
+
*/
|
|
448
|
+
export async function main() {
|
|
449
|
+
const opts = parseSyncTemplatesOptions();
|
|
450
|
+
const projectRoot = process.cwd();
|
|
451
|
+
// Check-drift mode: verify templates match source without syncing (read-only)
|
|
452
|
+
if (opts.checkDrift) {
|
|
453
|
+
console.log(`${LOG_PREFIX} Checking for template drift...`);
|
|
454
|
+
const drift = await checkTemplateDrift(projectRoot);
|
|
455
|
+
if (opts.verbose) {
|
|
456
|
+
console.log(` Checked ${drift.checkedFiles.length} files`);
|
|
457
|
+
}
|
|
458
|
+
if (drift.hasDrift) {
|
|
459
|
+
console.log(`\n${LOG_PREFIX} WARNING: Template drift detected!`);
|
|
460
|
+
console.log(' The following templates are out of sync with their source:');
|
|
461
|
+
for (const file of drift.driftingFiles) {
|
|
462
|
+
console.log(` - ${file}`);
|
|
463
|
+
}
|
|
464
|
+
console.log('\n Run `pnpm sync:templates` to update templates.');
|
|
465
|
+
process.exitCode = 1;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
console.log(`${LOG_PREFIX} All templates are in sync.`);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Dry-run mode: show what would be synced without writing
|
|
473
|
+
if (opts.dryRun) {
|
|
474
|
+
console.log(`${LOG_PREFIX} Dry-run mode - showing what would be synced...`);
|
|
475
|
+
const result = await syncTemplates(projectRoot, true);
|
|
476
|
+
const { totalSynced, totalErrors } = printSyncResults(result);
|
|
477
|
+
console.log(`\n${LOG_PREFIX} Dry run complete. Would sync ${totalSynced} files.`);
|
|
478
|
+
if (totalErrors > 0) {
|
|
479
|
+
console.log(` ${totalErrors} error(s) would occur.`);
|
|
480
|
+
process.exitCode = 1;
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
// Sync mode: update templates using micro-worktree isolation (WU-1368)
|
|
485
|
+
console.log(`${LOG_PREFIX} Syncing internal docs to CLI templates...`);
|
|
486
|
+
const result = await syncTemplatesWithWorktree(projectRoot);
|
|
487
|
+
const { totalSynced, totalErrors } = printSyncResults(result);
|
|
488
|
+
console.log(`\n${LOG_PREFIX} Done! Synced ${totalSynced} files.`);
|
|
335
489
|
if (totalErrors > 0) {
|
|
336
490
|
console.log(` ${totalErrors} error(s) occurred.`);
|
|
337
491
|
process.exitCode = 1;
|
package/dist/wu-block.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
2
|
/**
|
|
4
3
|
* WU Block Helper
|
|
5
4
|
*
|
|
@@ -29,7 +28,7 @@ import { readWU, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
|
|
|
29
28
|
import { BRANCHES, REMOTES, WU_STATUS, STATUS_SECTIONS, PATTERNS, LOG_PREFIX, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
30
29
|
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
31
30
|
import { ensureStaged } from '@lumenflow/core/dist/git-staged-validator.js';
|
|
32
|
-
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
31
|
+
import { withMicroWorktree, LUMENFLOW_WU_TOOL_ENV } from '@lumenflow/core/dist/micro-worktree.js';
|
|
33
32
|
import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
|
|
34
33
|
// WU-1603: Atomic lane locking - release lock when WU is blocked
|
|
35
34
|
import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
|
|
@@ -38,6 +37,30 @@ import { getLockPolicyForLane } from '@lumenflow/core/dist/lane-checker.js';
|
|
|
38
37
|
// ensureOnMain() moved to wu-helpers.ts (WU-1256)
|
|
39
38
|
// ensureStaged() moved to git-staged-validator.ts (WU-1341)
|
|
40
39
|
// defaultWorktreeFrom() moved to wu-paths.ts (WU-1341)
|
|
40
|
+
/**
|
|
41
|
+
* WU-1365: Execute a function with LUMENFLOW_WU_TOOL set, restoring afterwards
|
|
42
|
+
*
|
|
43
|
+
* Sets the LUMENFLOW_WU_TOOL env var to allow pre-push hook bypass, then
|
|
44
|
+
* restores the original value (or deletes it) after execution completes.
|
|
45
|
+
*
|
|
46
|
+
* @param toolName - Value to set for LUMENFLOW_WU_TOOL
|
|
47
|
+
* @param fn - Async function to execute
|
|
48
|
+
*/
|
|
49
|
+
async function withWuToolEnv(toolName, fn) {
|
|
50
|
+
const previousWuTool = process.env[LUMENFLOW_WU_TOOL_ENV];
|
|
51
|
+
process.env[LUMENFLOW_WU_TOOL_ENV] = toolName;
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
if (previousWuTool === undefined) {
|
|
57
|
+
Reflect.deleteProperty(process.env, LUMENFLOW_WU_TOOL_ENV);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
process.env[LUMENFLOW_WU_TOOL_ENV] = previousWuTool;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
41
64
|
/**
|
|
42
65
|
* Remove WU entry from in-progress section of lines array
|
|
43
66
|
*/
|
|
@@ -47,7 +70,6 @@ function removeFromInProgressSection(lines, inProgIdx, rel, id) {
|
|
|
47
70
|
let endIdx = lines.slice(inProgIdx + 1).findIndex((l) => l.startsWith('## '));
|
|
48
71
|
endIdx = endIdx === -1 ? lines.length : inProgIdx + 1 + endIdx;
|
|
49
72
|
for (let i = inProgIdx + 1; i < endIdx; i++) {
|
|
50
|
-
// eslint-disable-next-line security/detect-object-injection -- array index loop
|
|
51
73
|
if (lines[i] && (lines[i].includes(rel) || lines[i].includes(`[${id}`))) {
|
|
52
74
|
lines.splice(i, 1);
|
|
53
75
|
endIdx--;
|
|
@@ -58,6 +80,31 @@ function removeFromInProgressSection(lines, inProgIdx, rel, id) {
|
|
|
58
80
|
if (section.length === 0)
|
|
59
81
|
lines.splice(endIdx, 0, '', '(No items currently in progress)', '');
|
|
60
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* WU-1365: Create missing blocked section in status.md
|
|
85
|
+
*
|
|
86
|
+
* Extracts this logic to reduce cognitive complexity of moveFromInProgressToBlocked.
|
|
87
|
+
*
|
|
88
|
+
* @param lines - Array of lines from status.md
|
|
89
|
+
* @param inProgIdx - Index of "## In Progress" header (-1 if not found)
|
|
90
|
+
* @returns Index of the blocked section header after creation
|
|
91
|
+
*/
|
|
92
|
+
function createMissingBlockedSection(lines, inProgIdx) {
|
|
93
|
+
console.log(`${LOG_PREFIX.BLOCK} Creating missing "${STATUS_SECTIONS.BLOCKED}" section in status.md`);
|
|
94
|
+
// Find a good insertion point - after in_progress section or at end
|
|
95
|
+
const insertIdx = inProgIdx !== -1 ? inProgIdx + 1 : lines.length;
|
|
96
|
+
// Skip to end of in_progress section content if it exists
|
|
97
|
+
let insertPoint = insertIdx;
|
|
98
|
+
if (inProgIdx !== -1) {
|
|
99
|
+
// Find where the next section starts
|
|
100
|
+
const nextSectionIdx = lines.slice(inProgIdx + 1).findIndex((l) => l.startsWith('## '));
|
|
101
|
+
insertPoint = nextSectionIdx === -1 ? lines.length : inProgIdx + 1 + nextSectionIdx;
|
|
102
|
+
}
|
|
103
|
+
// Insert the blocked section
|
|
104
|
+
lines.splice(insertPoint, 0, '', STATUS_SECTIONS.BLOCKED, '');
|
|
105
|
+
// Return the index of the newly created section header
|
|
106
|
+
return insertPoint + 1;
|
|
107
|
+
}
|
|
61
108
|
async function moveFromInProgressToBlocked(statusPath, id, title, reason) {
|
|
62
109
|
// Check file exists
|
|
63
110
|
const fileExists = await access(statusPath)
|
|
@@ -66,23 +113,25 @@ async function moveFromInProgressToBlocked(statusPath, id, title, reason) {
|
|
|
66
113
|
if (!fileExists)
|
|
67
114
|
die(`Missing ${statusPath}`);
|
|
68
115
|
const rel = `wu/${id}.yaml`;
|
|
69
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
|
|
70
116
|
const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
71
117
|
const lines = content.split(/\r?\n/);
|
|
72
118
|
const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
|
|
73
119
|
const inProgIdx = findHeader(STATUS_SECTIONS.IN_PROGRESS);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
120
|
+
let blockedIdx = findHeader(STATUS_SECTIONS.BLOCKED);
|
|
121
|
+
// WU-1365: Handle missing blocked section gracefully by creating it
|
|
122
|
+
if (blockedIdx === -1) {
|
|
123
|
+
blockedIdx = createMissingBlockedSection(lines, inProgIdx);
|
|
124
|
+
}
|
|
77
125
|
removeFromInProgressSection(lines, inProgIdx, rel, id);
|
|
78
126
|
// Add bullet to blocked
|
|
79
127
|
const reasonSuffix = reason ? ` — ${reason}` : '';
|
|
80
128
|
const bullet = `- [${id} — ${title}](${rel})${reasonSuffix}`;
|
|
129
|
+
// Recalculate blockedIdx after removeFromInProgressSection may have changed line positions
|
|
130
|
+
blockedIdx = findHeader(STATUS_SECTIONS.BLOCKED);
|
|
81
131
|
const sectionStart = blockedIdx + 1;
|
|
82
132
|
if (lines.slice(sectionStart).some((l) => l.includes(rel)))
|
|
83
133
|
return; // already listed
|
|
84
134
|
lines.splice(sectionStart, 0, '', bullet);
|
|
85
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes status file
|
|
86
135
|
await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
|
|
87
136
|
encoding: FILE_SYSTEM.UTF8,
|
|
88
137
|
});
|
|
@@ -169,47 +218,50 @@ async function main() {
|
|
|
169
218
|
const baseMsg = `wu(${id.toLowerCase()}): block`;
|
|
170
219
|
const commitMsg = args.reason ? `${baseMsg} — ${args.reason}` : baseMsg;
|
|
171
220
|
if (!args.noAuto) {
|
|
172
|
-
//
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
// WU-1365: Set LUMENFLOW_WU_TOOL to allow pre-push hook bypass for micro-worktree pushes
|
|
222
|
+
await withWuToolEnv(MICRO_WORKTREE_OPERATIONS.WU_BLOCK, async () => {
|
|
223
|
+
// Use micro-worktree pattern to avoid pre-commit hook blocking commits to main
|
|
224
|
+
await withMicroWorktree({
|
|
225
|
+
operation: MICRO_WORKTREE_OPERATIONS.WU_BLOCK,
|
|
226
|
+
id,
|
|
227
|
+
logPrefix: LOG_PREFIX.BLOCK,
|
|
228
|
+
pushOnly: true, // Push directly to origin/main without touching local main
|
|
229
|
+
execute: async ({ worktreePath }) => {
|
|
230
|
+
// Build paths relative to micro-worktree
|
|
231
|
+
const microWUPath = path.join(worktreePath, WU_PATHS.WU(id));
|
|
232
|
+
const microStatusPath = path.join(worktreePath, WU_PATHS.STATUS());
|
|
233
|
+
const microBacklogPath = path.join(worktreePath, WU_PATHS.BACKLOG());
|
|
234
|
+
// Update WU YAML in micro-worktree
|
|
235
|
+
const microDoc = readWU(microWUPath, id);
|
|
236
|
+
microDoc.status = WU_STATUS.BLOCKED;
|
|
237
|
+
const noteLine = args.reason
|
|
238
|
+
? `Blocked (${todayISO()}): ${args.reason}`
|
|
239
|
+
: `Blocked (${todayISO()})`;
|
|
240
|
+
appendNote(microDoc, noteLine);
|
|
241
|
+
writeWU(microWUPath, microDoc);
|
|
242
|
+
// Update status.md in micro-worktree
|
|
243
|
+
await moveFromInProgressToBlocked(microStatusPath, id, title, args.reason);
|
|
244
|
+
// Update backlog.md in micro-worktree (WU-1574: regenerate from state store)
|
|
245
|
+
await regenerateBacklogFromState(microBacklogPath);
|
|
246
|
+
// Append block event to WUStateStore (WU-1573)
|
|
247
|
+
const stateDir = path.join(worktreePath, '.lumenflow', 'state');
|
|
248
|
+
const store = new WUStateStore(stateDir);
|
|
249
|
+
await store.load();
|
|
250
|
+
await store.block(id, args.reason || 'No reason provided');
|
|
251
|
+
return {
|
|
252
|
+
commitMessage: commitMsg,
|
|
253
|
+
files: [
|
|
254
|
+
WU_PATHS.WU(id),
|
|
255
|
+
WU_PATHS.STATUS(),
|
|
256
|
+
WU_PATHS.BACKLOG(),
|
|
257
|
+
'.lumenflow/state/wu-events.jsonl',
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
// Fetch to update local main tracking
|
|
263
|
+
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
210
264
|
});
|
|
211
|
-
// Fetch to update local main tracking
|
|
212
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
213
265
|
}
|
|
214
266
|
else {
|
|
215
267
|
// Manual mode: expect files already staged
|
package/dist/wu-claim.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
2
|
/**
|
|
4
3
|
* WU Claim Helper
|
|
5
4
|
*
|
|
@@ -27,9 +26,7 @@ import { checkLaneFree, validateLaneFormat, checkWipJustification, } from '@lume
|
|
|
27
26
|
import { acquireLaneLock, releaseLaneLock, checkLaneLock, forceRemoveStaleLock, } from '@lumenflow/core/dist/lane-lock.js';
|
|
28
27
|
// WU-1825: Import from unified code-path-validator (consolidates 3 validators)
|
|
29
28
|
// WU-1213: Using deprecated sync API - async validate() requires larger refactor (separate WU)
|
|
30
|
-
import {
|
|
31
|
-
// eslint-disable-next-line sonarjs/deprecation -- sync API required for current architecture
|
|
32
|
-
validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/dist/code-path-validator.js';
|
|
29
|
+
import { validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/dist/code-path-validator.js';
|
|
33
30
|
// WU-1574: parseBacklogFrontmatter/getSectionHeadings removed - state store replaces backlog parsing
|
|
34
31
|
import { detectConflicts } from '@lumenflow/core/dist/code-paths-overlap.js';
|
|
35
32
|
import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
|
|
@@ -86,7 +83,6 @@ const PREFIX = LOG_PREFIX.CLAIM;
|
|
|
86
83
|
*/
|
|
87
84
|
function preflightValidateWU(WU_PATH, id) {
|
|
88
85
|
// Check file exists
|
|
89
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
90
86
|
if (!existsSync(WU_PATH)) {
|
|
91
87
|
die(`WU file not found: ${WU_PATH}\n\n` +
|
|
92
88
|
`Cannot claim a WU that doesn't exist.\n\n` +
|
|
@@ -96,7 +92,6 @@ function preflightValidateWU(WU_PATH, id) {
|
|
|
96
92
|
` 3. Check if the WU file was moved or deleted`);
|
|
97
93
|
}
|
|
98
94
|
// Parse and validate YAML structure
|
|
99
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
100
95
|
const text = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
|
|
101
96
|
let doc;
|
|
102
97
|
try {
|
|
@@ -189,7 +184,6 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
|
|
|
189
184
|
// Read file
|
|
190
185
|
let text;
|
|
191
186
|
try {
|
|
192
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
193
187
|
text = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
|
|
194
188
|
}
|
|
195
189
|
catch (e) {
|
|
@@ -266,7 +260,6 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
|
|
|
266
260
|
// WU-1352: Use centralized stringify for consistent output
|
|
267
261
|
const out = stringifyYAML(doc);
|
|
268
262
|
// Write file
|
|
269
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
|
|
270
263
|
await writeFile(WU_PATH, out, { encoding: FILE_SYSTEM.UTF8 });
|
|
271
264
|
// WU-1211: Return both title and initiative for status progression check
|
|
272
265
|
return { title: doc.title || '', initiative: doc.initiative || null };
|
|
@@ -326,7 +319,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
|
|
|
326
319
|
const rel = `wu/${id}.yaml`;
|
|
327
320
|
const bullet = `- [${id} — ${title}](${rel})`;
|
|
328
321
|
// Read file
|
|
329
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
|
|
330
322
|
const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
331
323
|
const lines = content.split(STRING_LITERALS.NEWLINE);
|
|
332
324
|
const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
|
|
@@ -344,7 +336,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
|
|
|
344
336
|
return; // already listed
|
|
345
337
|
// Remove "No items" marker if present
|
|
346
338
|
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
347
|
-
// eslint-disable-next-line security/detect-object-injection -- array index loop
|
|
348
339
|
if (lines[i] && lines[i].includes('No items currently in progress')) {
|
|
349
340
|
lines.splice(i, 1);
|
|
350
341
|
endIdx--;
|
|
@@ -354,7 +345,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
|
|
|
354
345
|
// Insert bullet right after header
|
|
355
346
|
lines.splice(startIdx + 1, 0, '', bullet);
|
|
356
347
|
// Write file
|
|
357
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes status file
|
|
358
348
|
await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
|
|
359
349
|
encoding: FILE_SYSTEM.UTF8,
|
|
360
350
|
});
|
|
@@ -444,10 +434,8 @@ async function applyStagedChangesToMicroWorktree(worktreePath, stagedChanges) {
|
|
|
444
434
|
continue;
|
|
445
435
|
}
|
|
446
436
|
const sourcePath = path.join(process.cwd(), filePath);
|
|
447
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
|
|
448
437
|
const contents = await readFile(sourcePath, { encoding: FILE_SYSTEM.UTF8 });
|
|
449
438
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
450
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
|
|
451
439
|
await writeFile(targetPath, contents, { encoding: FILE_SYSTEM.UTF8 });
|
|
452
440
|
}
|
|
453
441
|
}
|
|
@@ -520,11 +508,9 @@ async function readWUTitle(id) {
|
|
|
520
508
|
return null;
|
|
521
509
|
}
|
|
522
510
|
// Read file
|
|
523
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
524
511
|
const text = await readFile(p, { encoding: FILE_SYSTEM.UTF8 });
|
|
525
512
|
// Match title field - use RegExp.exec for sonarjs/prefer-regexp-exec compliance
|
|
526
513
|
// Regex is safe: runs on trusted WU YAML files with bounded input
|
|
527
|
-
// eslint-disable-next-line sonarjs/slow-regex -- Bounded input from WU YAML files
|
|
528
514
|
const titlePattern = /^title:\s*"?([^"\n]+)"?$/m;
|
|
529
515
|
const m = titlePattern.exec(text);
|
|
530
516
|
return m ? m[1] : null;
|
|
@@ -546,7 +532,6 @@ async function checkExistingBranchOnlyWU(statusPath, currentWU) {
|
|
|
546
532
|
return { hasBranchOnly: false, existingWU: null };
|
|
547
533
|
}
|
|
548
534
|
// Read file
|
|
549
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
|
|
550
535
|
const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
551
536
|
const lines = content.split(STRING_LITERALS.NEWLINE);
|
|
552
537
|
// Find "In Progress" section
|
|
@@ -581,7 +566,6 @@ async function checkExistingBranchOnlyWU(statusPath, currentWU) {
|
|
|
581
566
|
}
|
|
582
567
|
try {
|
|
583
568
|
// Read file
|
|
584
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
585
569
|
const text = await readFile(wuPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
586
570
|
const doc = parseYAML(text);
|
|
587
571
|
if (doc && doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY) {
|
|
@@ -705,10 +689,8 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
|
|
|
705
689
|
* Handle code path overlap detection (WU-901)
|
|
706
690
|
*/
|
|
707
691
|
function handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args) {
|
|
708
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
709
692
|
if (!existsSync(WU_PATH))
|
|
710
693
|
return;
|
|
711
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
712
694
|
const wuContent = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
|
|
713
695
|
const wuDoc = parseYAML(wuContent);
|
|
714
696
|
const codePaths = wuDoc.code_paths || [];
|
|
@@ -846,7 +828,6 @@ async function claimBranchOnlyMode(ctx) {
|
|
|
846
828
|
console.log(`\n${PREFIX} For sub-agent execution:`);
|
|
847
829
|
console.log(` /wu-prompt ${id} (generates full context prompt)`);
|
|
848
830
|
// Emit mandatory agent advisory based on code_paths (WU-1324)
|
|
849
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
850
831
|
const wuContent = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
|
|
851
832
|
const wuDoc = parseYAML(wuContent);
|
|
852
833
|
const codePaths = wuDoc.code_paths || [];
|
|
@@ -1008,7 +989,6 @@ async function claimWorktreeMode(ctx) {
|
|
|
1008
989
|
// Emit mandatory agent advisory based on code_paths (WU-1324)
|
|
1009
990
|
// Read from worktree since that's where the updated YAML is
|
|
1010
991
|
const wtWUPathForAdvisory = path.join(worktreePath, WU_PATH);
|
|
1011
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
1012
992
|
const wuContent = await readFile(wtWUPathForAdvisory, {
|
|
1013
993
|
encoding: FILE_SYSTEM.UTF8,
|
|
1014
994
|
});
|
|
@@ -1226,7 +1206,6 @@ async function main() {
|
|
|
1226
1206
|
console.warn(`${PREFIX} ${wipJustificationCheck.warning}`);
|
|
1227
1207
|
}
|
|
1228
1208
|
// WU-1372: Lane-to-code_paths consistency check (advisory only, never blocks)
|
|
1229
|
-
// eslint-disable-next-line sonarjs/deprecation -- sync API required for current architecture
|
|
1230
1209
|
const laneValidation = validateLaneCodePaths(doc, args.lane);
|
|
1231
1210
|
logLaneValidationWarnings(laneValidation, PREFIX);
|
|
1232
1211
|
// WU-1361: YAML schema validation at claim time
|
package/dist/wu-cleanup.js
CHANGED
|
@@ -29,7 +29,6 @@ import { isGhCliAvailable } from '@lumenflow/core/dist/wu-done-pr.js';
|
|
|
29
29
|
import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
30
30
|
// WU-2278: Import ownership validation for cross-agent protection
|
|
31
31
|
import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
|
|
32
|
-
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
33
32
|
const CLEANUP_OPTIONS = {
|
|
34
33
|
artifacts: {
|
|
35
34
|
name: 'artifacts',
|
package/dist/wu-create.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
2
|
/**
|
|
4
3
|
* WU Create Helper (WU-1262, WU-1439)
|
|
5
4
|
*
|
|
@@ -491,7 +490,6 @@ async function getDefaultAssignedTo() {
|
|
|
491
490
|
return '';
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- main() orchestrates multi-step WU creation workflow
|
|
495
493
|
async function main() {
|
|
496
494
|
const args = createWUParser({
|
|
497
495
|
name: 'wu-create',
|