@lumenflow/cli 1.0.0 → 1.1.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/dist/__tests__/flow-report.test.js +24 -0
- package/dist/__tests__/metrics-snapshot.test.js +24 -0
- package/dist/agent-issues-query.js +251 -0
- package/dist/agent-log-issue.js +67 -0
- package/dist/agent-session-end.js +36 -0
- package/dist/agent-session.js +46 -0
- package/dist/flow-bottlenecks.js +183 -0
- package/dist/flow-report.js +311 -0
- package/dist/gates.js +24 -10
- package/dist/init.js +251 -0
- package/dist/initiative-bulk-assign-wus.js +315 -0
- package/dist/metrics-snapshot.js +314 -0
- package/dist/orchestrate-init-status.js +64 -0
- package/dist/orchestrate-initiative.js +100 -0
- package/dist/orchestrate-monitor.js +90 -0
- package/dist/wu-claim.js +18 -7
- package/dist/wu-delete.js +241 -0
- package/dist/wu-done.js +102 -10
- package/dist/wu-unlock-lane.js +158 -0
- package/package.json +24 -4
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrate Initiative CLI
|
|
4
|
+
*
|
|
5
|
+
* Orchestrate initiative execution with parallel agent spawning.
|
|
6
|
+
* Builds execution plan based on WU dependencies and manages wave-based execution.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm orchestrate:initiative --initiative INIT-001
|
|
10
|
+
* pnpm orchestrate:initiative --initiative INIT-001 --dry-run
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlan, formatExecutionPlan, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointMode, LOG_PREFIX, } from '@lumenflow/initiatives';
|
|
15
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
16
|
+
const program = new Command()
|
|
17
|
+
.name('orchestrate-initiative')
|
|
18
|
+
.description('Orchestrate initiative execution with parallel agent spawning')
|
|
19
|
+
.option('-i, --initiative <ids...>', 'Initiative ID(s) to orchestrate')
|
|
20
|
+
.option('-d, --dry-run', 'Show execution plan without spawning agents')
|
|
21
|
+
.option('-p, --progress', 'Show current progress only')
|
|
22
|
+
.option('-c, --checkpoint-per-wave', 'Spawn next wave then exit (no polling)')
|
|
23
|
+
.option('--no-checkpoint', 'Force polling mode')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
const { initiative: initIds, dryRun, progress: progressOnly, checkpointPerWave, checkpoint, } = options;
|
|
26
|
+
const noCheckpoint = checkpoint === false;
|
|
27
|
+
try {
|
|
28
|
+
validateCheckpointFlags({ checkpointPerWave, dryRun, noCheckpoint });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${error.message}`));
|
|
32
|
+
process.exit(EXIT_CODES.ERROR);
|
|
33
|
+
}
|
|
34
|
+
if (!initIds || initIds.length === 0) {
|
|
35
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: --initiative is required`));
|
|
36
|
+
console.error('');
|
|
37
|
+
console.error('Usage:');
|
|
38
|
+
console.error(' pnpm orchestrate:initiative --initiative INIT-001');
|
|
39
|
+
console.error(' pnpm orchestrate:initiative --initiative INIT-001 --dry-run');
|
|
40
|
+
process.exit(EXIT_CODES.ERROR);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading initiative(s): ${initIds.join(', ')}`));
|
|
44
|
+
let wus;
|
|
45
|
+
let initiative;
|
|
46
|
+
if (initIds.length === 1) {
|
|
47
|
+
const result = loadInitiativeWUs(initIds[0]);
|
|
48
|
+
initiative = result.initiative;
|
|
49
|
+
wus = result.wus;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
wus = loadMultipleInitiatives(initIds);
|
|
53
|
+
initiative = { id: 'MULTI', title: `Combined: ${initIds.join(', ')}` };
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.green(`${LOG_PREFIX} Loaded ${wus.length} WU(s)`));
|
|
56
|
+
console.log('');
|
|
57
|
+
const progress = calculateProgress(wus);
|
|
58
|
+
console.log(chalk.bold('Progress:'));
|
|
59
|
+
console.log(formatProgress(progress));
|
|
60
|
+
console.log('');
|
|
61
|
+
if (progressOnly) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const checkpointDecision = resolveCheckpointMode({ checkpointPerWave, noCheckpoint, dryRun }, wus);
|
|
65
|
+
if (checkpointDecision.enabled) {
|
|
66
|
+
if (initIds.length > 1) {
|
|
67
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: Checkpoint mode only supports single initiative`));
|
|
68
|
+
process.exit(EXIT_CODES.ERROR);
|
|
69
|
+
}
|
|
70
|
+
const waveData = buildCheckpointWave(initIds[0], { dryRun });
|
|
71
|
+
if (!waveData) {
|
|
72
|
+
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to spawn.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(formatCheckpointOutput({ ...waveData, dryRun }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Building execution plan...`));
|
|
79
|
+
const plan = buildExecutionPlan(wus);
|
|
80
|
+
if (plan.waves.length === 0) {
|
|
81
|
+
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to execute.`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.bold('Execution Plan:'));
|
|
86
|
+
console.log(formatExecutionPlan(initiative, plan));
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log(chalk.yellow(`${LOG_PREFIX} Dry run mode - no agents spawned`));
|
|
89
|
+
console.log(chalk.cyan('To execute this plan, remove the --dry-run flag.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.green(`${LOG_PREFIX} Execution plan output complete.`));
|
|
93
|
+
console.log(chalk.cyan('Copy the spawn commands above to execute.'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${error.message}`));
|
|
97
|
+
process.exit(EXIT_CODES.ERROR);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
program.parse();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrate Monitor CLI
|
|
4
|
+
*
|
|
5
|
+
* Monitors spawned agent progress using mem:inbox signals.
|
|
6
|
+
* Designed to prevent context exhaustion by using compact signal output.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm orchestrate:monitor --since 30m
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import ms from 'ms';
|
|
17
|
+
const LOG_PREFIX = '[orchestrate:monitor]';
|
|
18
|
+
const MEMORY_DIR = '.beacon/memory';
|
|
19
|
+
function parseTimeString(timeStr) {
|
|
20
|
+
const msValue = ms(timeStr);
|
|
21
|
+
if (typeof msValue === 'number') {
|
|
22
|
+
return new Date(Date.now() - msValue);
|
|
23
|
+
}
|
|
24
|
+
const date = new Date(timeStr);
|
|
25
|
+
if (isNaN(date.getTime())) {
|
|
26
|
+
throw new Error(`Invalid time format: ${timeStr}`);
|
|
27
|
+
}
|
|
28
|
+
return date;
|
|
29
|
+
}
|
|
30
|
+
function loadRecentSignals(since) {
|
|
31
|
+
const signals = [];
|
|
32
|
+
if (!existsSync(MEMORY_DIR)) {
|
|
33
|
+
return signals;
|
|
34
|
+
}
|
|
35
|
+
const files = readdirSync(MEMORY_DIR).filter((f) => f.endsWith('.ndjson'));
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const filePath = join(MEMORY_DIR, file);
|
|
38
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
39
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
try {
|
|
42
|
+
const signal = JSON.parse(line);
|
|
43
|
+
const signalTime = new Date(signal.timestamp);
|
|
44
|
+
if (signalTime >= since) {
|
|
45
|
+
signals.push(signal);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Skip malformed lines
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return signals.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
54
|
+
}
|
|
55
|
+
const program = new Command()
|
|
56
|
+
.name('orchestrate:monitor')
|
|
57
|
+
.description('Monitor spawned agent progress')
|
|
58
|
+
.option('--since <time>', 'Show signals since (e.g., 30m, 1h)', '30m')
|
|
59
|
+
.option('--wu <id>', 'Filter by WU ID')
|
|
60
|
+
.action((opts) => {
|
|
61
|
+
try {
|
|
62
|
+
const since = parseTimeString(opts.since);
|
|
63
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading signals since ${since.toISOString()}...`));
|
|
64
|
+
const signals = loadRecentSignals(since);
|
|
65
|
+
if (signals.length === 0) {
|
|
66
|
+
console.log(chalk.yellow(`${LOG_PREFIX} No signals found.`));
|
|
67
|
+
console.log(chalk.gray('Agents may still be starting up, or memory layer not initialized.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const filtered = opts.wu ? signals.filter((s) => s.wuId === opts.wu) : signals;
|
|
71
|
+
console.log(chalk.bold(`\nRecent Signals (${filtered.length}):\n`));
|
|
72
|
+
for (const signal of filtered) {
|
|
73
|
+
const time = new Date(signal.timestamp).toLocaleTimeString();
|
|
74
|
+
const wu = signal.wuId ? chalk.cyan(signal.wuId) : chalk.gray('system');
|
|
75
|
+
const type = signal.type === 'complete'
|
|
76
|
+
? chalk.green(signal.type)
|
|
77
|
+
: signal.type === 'error'
|
|
78
|
+
? chalk.red(signal.type)
|
|
79
|
+
: chalk.yellow(signal.type);
|
|
80
|
+
console.log(` ${chalk.gray(time)} [${wu}] ${type}: ${signal.message || ''}`);
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.gray(`Use: pnpm mem:inbox --since ${opts.since} for more details`));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${err.message}`));
|
|
87
|
+
process.exit(EXIT_CODES.ERROR);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
program.parse();
|
package/dist/wu-claim.js
CHANGED
|
@@ -507,7 +507,10 @@ function validateLaneFormatWithError(lane) {
|
|
|
507
507
|
}
|
|
508
508
|
}
|
|
509
509
|
/**
|
|
510
|
-
* Handle lane occupancy check and enforce WIP
|
|
510
|
+
* Handle lane occupancy check and enforce WIP limit policy
|
|
511
|
+
*
|
|
512
|
+
* WU-1016: Updated to support configurable WIP limits per lane.
|
|
513
|
+
* The WIP limit is read from .lumenflow.config.yaml and defaults to 1.
|
|
511
514
|
*/
|
|
512
515
|
function handleLaneOccupancy(laneCheck, lane, id, force) {
|
|
513
516
|
if (laneCheck.free)
|
|
@@ -517,18 +520,26 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
|
|
|
517
520
|
}
|
|
518
521
|
if (!laneCheck.occupiedBy)
|
|
519
522
|
return;
|
|
523
|
+
// WU-1016: Include WIP limit info in messages
|
|
524
|
+
const wipLimit = laneCheck.wipLimit ?? 1;
|
|
525
|
+
const currentCount = laneCheck.currentCount ?? 0;
|
|
526
|
+
const inProgressList = laneCheck.inProgressWUs?.join(', ') || laneCheck.occupiedBy;
|
|
520
527
|
if (force) {
|
|
521
|
-
console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}"
|
|
522
|
-
console.warn(`${PREFIX} ⚠️
|
|
528
|
+
console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}" has ${currentCount}/${wipLimit} WUs in progress`);
|
|
529
|
+
console.warn(`${PREFIX} ⚠️ In progress: ${inProgressList}`);
|
|
530
|
+
console.warn(`${PREFIX} ⚠️ Forcing WIP limit override. Risk of worktree collision!`);
|
|
523
531
|
console.warn(`${PREFIX} ⚠️ Use only for P0 emergencies or manual recovery.`);
|
|
524
532
|
return;
|
|
525
533
|
}
|
|
526
|
-
die(`Lane "${lane}" is
|
|
527
|
-
`
|
|
534
|
+
die(`Lane "${lane}" is at WIP limit (${currentCount}/${wipLimit}).\n\n` +
|
|
535
|
+
`In progress: ${inProgressList}\n\n` +
|
|
536
|
+
`LumenFlow enforces WIP limits per lane to maintain focus.\n` +
|
|
537
|
+
`Current limit for "${lane}": ${wipLimit} (configure in .lumenflow.config.yaml)\n\n` +
|
|
528
538
|
`Options:\n` +
|
|
529
|
-
` 1. Wait for
|
|
539
|
+
` 1. Wait for a WU to complete or block\n` +
|
|
530
540
|
` 2. Choose a different lane\n` +
|
|
531
|
-
` 3.
|
|
541
|
+
` 3. Increase wip_limit in .lumenflow.config.yaml\n` +
|
|
542
|
+
` 4. Use --force to override (P0 emergencies only)\n\n` +
|
|
532
543
|
`To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
|
|
533
544
|
}
|
|
534
545
|
/**
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WU Delete Helper
|
|
4
|
+
*
|
|
5
|
+
* Race-safe WU deletion using micro-worktree isolation.
|
|
6
|
+
*
|
|
7
|
+
* Uses micro-worktree pattern:
|
|
8
|
+
* 1) Validate inputs (WU exists, status is not in_progress)
|
|
9
|
+
* 2) Ensure main is clean and up-to-date with origin
|
|
10
|
+
* 3) Create temp branch WITHOUT switching (main checkout stays on main)
|
|
11
|
+
* 4) Create micro-worktree in /tmp pointing to temp branch
|
|
12
|
+
* 5) Delete WU file and update backlog.md in micro-worktree
|
|
13
|
+
* 6) Commit, ff-only merge, push
|
|
14
|
+
* 7) Cleanup temp branch and micro-worktree
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* pnpm wu:delete --id WU-123 # Single WU deletion
|
|
18
|
+
* pnpm wu:delete --id WU-123 --dry-run # Dry run
|
|
19
|
+
* pnpm wu:delete --batch WU-1,WU-2,WU-3 # Batch deletion
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
24
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
25
|
+
import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
26
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
27
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
28
|
+
import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
29
|
+
import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat, } from '@lumenflow/core/dist/wu-helpers.js';
|
|
30
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
31
|
+
const PREFIX = LOG_PREFIX.DELETE || '[wu:delete]';
|
|
32
|
+
const DELETE_OPTIONS = {
|
|
33
|
+
dryRun: {
|
|
34
|
+
name: 'dryRun',
|
|
35
|
+
flags: '--dry-run',
|
|
36
|
+
description: 'Show what would be deleted without making changes',
|
|
37
|
+
},
|
|
38
|
+
batch: {
|
|
39
|
+
name: 'batch',
|
|
40
|
+
flags: '--batch <ids>',
|
|
41
|
+
description: 'Delete multiple WUs atomically (comma-separated: WU-1,WU-2,WU-3)',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
function parseArgs() {
|
|
45
|
+
return createWUParser({
|
|
46
|
+
name: 'wu-delete',
|
|
47
|
+
description: 'Safely delete WU YAML files with micro-worktree isolation',
|
|
48
|
+
options: [WU_OPTIONS.id, DELETE_OPTIONS.dryRun, DELETE_OPTIONS.batch],
|
|
49
|
+
required: [],
|
|
50
|
+
allowPositionalId: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function parseBatchIds(batchArg) {
|
|
54
|
+
return batchArg
|
|
55
|
+
.split(',')
|
|
56
|
+
.map((id) => id.trim().toUpperCase())
|
|
57
|
+
.filter((id) => id.length > 0);
|
|
58
|
+
}
|
|
59
|
+
function validateWUDeletable(id) {
|
|
60
|
+
const wuPath = WU_PATHS.WU(id);
|
|
61
|
+
if (!existsSync(wuPath)) {
|
|
62
|
+
die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
|
|
63
|
+
}
|
|
64
|
+
const content = readFileSync(wuPath, FILE_SYSTEM.ENCODING);
|
|
65
|
+
const wu = parseYAML(content);
|
|
66
|
+
if (wu.status === WU_STATUS.IN_PROGRESS) {
|
|
67
|
+
die(`Cannot delete WU ${id}: status is '${WU_STATUS.IN_PROGRESS}'.\n\n` +
|
|
68
|
+
`WUs that are actively being worked on cannot be deleted.\n` +
|
|
69
|
+
`If the WU was abandoned, first run: pnpm wu:block --id ${id} --reason "Abandoned"\n` +
|
|
70
|
+
`Then retry the delete operation.`);
|
|
71
|
+
}
|
|
72
|
+
return { wu, wuPath };
|
|
73
|
+
}
|
|
74
|
+
async function ensureCleanWorkingTree() {
|
|
75
|
+
const status = await getGitForCwd().getStatus();
|
|
76
|
+
if (status.trim()) {
|
|
77
|
+
die(`Working tree is not clean. Cannot delete WU.\n\nUncommitted changes:\n${status}\n\nCommit or stash changes before deleting:\n git add . && git commit -m "..."\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function getStampPath(id) {
|
|
81
|
+
return join(WU_PATHS.STAMPS_DIR(), `${id}.done`);
|
|
82
|
+
}
|
|
83
|
+
function stampExists(id) {
|
|
84
|
+
return existsSync(getStampPath(id));
|
|
85
|
+
}
|
|
86
|
+
function removeFromBacklog(backlogPath, id) {
|
|
87
|
+
if (!existsSync(backlogPath)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const content = readFileSync(backlogPath, FILE_SYSTEM.ENCODING);
|
|
91
|
+
const wuLinkPattern = new RegExp(`^.*\\[${id}[^\\]]*\\].*$`, 'gmi');
|
|
92
|
+
const wuSimplePattern = new RegExp(`^.*${id}.*\\.yaml.*$`, 'gmi');
|
|
93
|
+
let updated = content.replace(wuLinkPattern, '');
|
|
94
|
+
updated = updated.replace(wuSimplePattern, '');
|
|
95
|
+
updated = updated.replace(/\n{3,}/g, '\n\n');
|
|
96
|
+
if (updated !== content) {
|
|
97
|
+
writeFileSync(backlogPath, updated, FILE_SYSTEM.ENCODING);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
async function deleteSingleWU(id, dryRun) {
|
|
103
|
+
console.log(`${PREFIX} Starting WU delete for ${id}`);
|
|
104
|
+
validateWUIDFormat(id);
|
|
105
|
+
const { wu, wuPath } = validateWUDeletable(id);
|
|
106
|
+
console.log(`${PREFIX} WU details:`);
|
|
107
|
+
console.log(`${PREFIX} Title: ${wu.title}`);
|
|
108
|
+
console.log(`${PREFIX} Lane: ${wu.lane}`);
|
|
109
|
+
console.log(`${PREFIX} Status: ${wu.status}`);
|
|
110
|
+
console.log(`${PREFIX} Path: ${wuPath}`);
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
console.log(`\n${PREFIX} 🔍 DRY RUN: Would delete ${id}`);
|
|
113
|
+
console.log(`${PREFIX} - Delete file: ${wuPath}`);
|
|
114
|
+
console.log(`${PREFIX} - Update: ${WU_PATHS.BACKLOG()}`);
|
|
115
|
+
if (stampExists(id)) {
|
|
116
|
+
console.log(`${PREFIX} - Delete stamp: ${getStampPath(id)}`);
|
|
117
|
+
}
|
|
118
|
+
console.log(`${PREFIX} No changes made.`);
|
|
119
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
120
|
+
}
|
|
121
|
+
await ensureOnMain(getGitForCwd());
|
|
122
|
+
await ensureCleanWorkingTree();
|
|
123
|
+
await ensureMainUpToDate(getGitForCwd(), 'wu:delete');
|
|
124
|
+
console.log(`${PREFIX} Deleting via micro-worktree...`);
|
|
125
|
+
await withMicroWorktree({
|
|
126
|
+
operation: MICRO_WORKTREE_OPERATIONS.WU_DELETE,
|
|
127
|
+
id: id,
|
|
128
|
+
logPrefix: PREFIX,
|
|
129
|
+
execute: async ({ worktreePath, gitWorktree }) => {
|
|
130
|
+
const wuFilePath = join(worktreePath, wuPath);
|
|
131
|
+
const backlogFilePath = join(worktreePath, WU_PATHS.BACKLOG());
|
|
132
|
+
unlinkSync(wuFilePath);
|
|
133
|
+
console.log(`${PREFIX} ✅ Deleted ${id}.yaml`);
|
|
134
|
+
const stampPath = join(worktreePath, getStampPath(id));
|
|
135
|
+
if (existsSync(stampPath)) {
|
|
136
|
+
unlinkSync(stampPath);
|
|
137
|
+
console.log(`${PREFIX} ✅ Deleted stamp ${id}.done`);
|
|
138
|
+
}
|
|
139
|
+
const removedFromBacklog = removeFromBacklog(backlogFilePath, id);
|
|
140
|
+
if (removedFromBacklog) {
|
|
141
|
+
console.log(`${PREFIX} ✅ Removed ${id} from backlog.md`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`${PREFIX} ℹ️ ${id} was not found in backlog.md`);
|
|
145
|
+
}
|
|
146
|
+
await gitWorktree.add('.');
|
|
147
|
+
const commitMessage = `docs: delete ${id.toLowerCase()}`;
|
|
148
|
+
return {
|
|
149
|
+
commitMessage,
|
|
150
|
+
files: [],
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
console.log(`${PREFIX} ✅ Successfully deleted ${id}`);
|
|
155
|
+
console.log(`${PREFIX} Changes pushed to origin/main`);
|
|
156
|
+
}
|
|
157
|
+
async function deleteBatchWUs(ids, dryRun) {
|
|
158
|
+
console.log(`${PREFIX} Starting batch delete for ${ids.length} WU(s): ${ids.join(', ')}`);
|
|
159
|
+
const wusToDelete = [];
|
|
160
|
+
const stampsToDelete = [];
|
|
161
|
+
for (const id of ids) {
|
|
162
|
+
validateWUIDFormat(id);
|
|
163
|
+
const { wu, wuPath } = validateWUDeletable(id);
|
|
164
|
+
wusToDelete.push({ id, wu, wuPath });
|
|
165
|
+
if (stampExists(id)) {
|
|
166
|
+
stampsToDelete.push(id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log(`${PREFIX} WUs to delete:`);
|
|
170
|
+
for (const { id, wu, wuPath } of wusToDelete) {
|
|
171
|
+
console.log(`${PREFIX} ${id}: ${wu.title} (${wu.status}) - ${wuPath}`);
|
|
172
|
+
}
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
console.log(`\n${PREFIX} 🔍 DRY RUN: Would delete ${ids.length} WU(s)`);
|
|
175
|
+
console.log(`${PREFIX} No changes made.`);
|
|
176
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
177
|
+
}
|
|
178
|
+
await ensureOnMain(getGitForCwd());
|
|
179
|
+
await ensureCleanWorkingTree();
|
|
180
|
+
await ensureMainUpToDate(getGitForCwd(), 'wu:delete --batch');
|
|
181
|
+
console.log(`${PREFIX} Deleting ${ids.length} WU(s) via micro-worktree...`);
|
|
182
|
+
await withMicroWorktree({
|
|
183
|
+
operation: MICRO_WORKTREE_OPERATIONS.WU_DELETE,
|
|
184
|
+
id: `batch-${ids.length}`,
|
|
185
|
+
logPrefix: PREFIX,
|
|
186
|
+
execute: async ({ worktreePath, gitWorktree }) => {
|
|
187
|
+
const backlogFilePath = join(worktreePath, WU_PATHS.BACKLOG());
|
|
188
|
+
for (const { id, wuPath } of wusToDelete) {
|
|
189
|
+
const wuFilePath = join(worktreePath, wuPath);
|
|
190
|
+
unlinkSync(wuFilePath);
|
|
191
|
+
console.log(`${PREFIX} ✅ Deleted ${id}.yaml`);
|
|
192
|
+
}
|
|
193
|
+
for (const id of stampsToDelete) {
|
|
194
|
+
const stampPath = join(worktreePath, getStampPath(id));
|
|
195
|
+
if (existsSync(stampPath)) {
|
|
196
|
+
unlinkSync(stampPath);
|
|
197
|
+
console.log(`${PREFIX} ✅ Deleted stamp ${id}.done`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const { id } of wusToDelete) {
|
|
201
|
+
const removed = removeFromBacklog(backlogFilePath, id);
|
|
202
|
+
if (removed) {
|
|
203
|
+
console.log(`${PREFIX} ✅ Removed ${id} from backlog.md`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await gitWorktree.add('.');
|
|
207
|
+
const idList = ids.map((id) => id.toLowerCase()).join(', ');
|
|
208
|
+
const commitMessage = `chore(repair): delete ${ids.length} orphaned wus (${idList})`;
|
|
209
|
+
return {
|
|
210
|
+
commitMessage,
|
|
211
|
+
files: [],
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
console.log(`${PREFIX} ✅ Successfully deleted ${ids.length} WU(s)`);
|
|
216
|
+
console.log(`${PREFIX} Changes pushed to origin/main`);
|
|
217
|
+
}
|
|
218
|
+
async function main() {
|
|
219
|
+
const opts = parseArgs();
|
|
220
|
+
const { id, dryRun, batch } = opts;
|
|
221
|
+
if (!id && !batch) {
|
|
222
|
+
die('Must specify either --id WU-XXX or --batch WU-1,WU-2,WU-3');
|
|
223
|
+
}
|
|
224
|
+
if (id && batch) {
|
|
225
|
+
die('Cannot use both --id and --batch. Use one or the other.');
|
|
226
|
+
}
|
|
227
|
+
if (batch) {
|
|
228
|
+
const ids = parseBatchIds(batch);
|
|
229
|
+
if (ids.length === 0) {
|
|
230
|
+
die('--batch requires at least one WU ID');
|
|
231
|
+
}
|
|
232
|
+
await deleteBatchWUs(ids, dryRun);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
await deleteSingleWU(id, dryRun);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error(`${PREFIX} ❌ ${err.message}`);
|
|
240
|
+
process.exit(EXIT_CODES.ERROR);
|
|
241
|
+
});
|
package/dist/wu-done.js
CHANGED
|
@@ -174,6 +174,77 @@ export function validateAccessibilityOrDie(wu, options = {}) {
|
|
|
174
174
|
`This gate prevents "orphaned code" - features that exist but users cannot access.`);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
export function validateDocsOnlyFlag(wu, args) {
|
|
178
|
+
// If --docs-only flag is not used, no validation needed
|
|
179
|
+
if (!args.docsOnly) {
|
|
180
|
+
return { valid: true, errors: [] };
|
|
181
|
+
}
|
|
182
|
+
const wuId = wu.id || 'unknown';
|
|
183
|
+
const exposure = wu.exposure;
|
|
184
|
+
const type = wu.type;
|
|
185
|
+
const codePaths = wu.code_paths;
|
|
186
|
+
// Check 1: exposure is 'documentation'
|
|
187
|
+
if (exposure === 'documentation') {
|
|
188
|
+
return { valid: true, errors: [] };
|
|
189
|
+
}
|
|
190
|
+
// Check 2: type is 'documentation'
|
|
191
|
+
if (type === 'documentation') {
|
|
192
|
+
return { valid: true, errors: [] };
|
|
193
|
+
}
|
|
194
|
+
// Check 3: all code_paths are documentation paths
|
|
195
|
+
const DOCS_ONLY_PREFIXES = ['docs/', 'ai/', '.claude/', 'memory-bank/'];
|
|
196
|
+
const DOCS_ONLY_ROOT_FILES = ['readme', 'claude'];
|
|
197
|
+
const isDocsPath = (p) => {
|
|
198
|
+
const path = p.trim().toLowerCase();
|
|
199
|
+
// Check docs prefixes
|
|
200
|
+
for (const prefix of DOCS_ONLY_PREFIXES) {
|
|
201
|
+
if (path.startsWith(prefix))
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Check markdown files
|
|
205
|
+
if (path.endsWith('.md'))
|
|
206
|
+
return true;
|
|
207
|
+
// Check root file patterns
|
|
208
|
+
for (const pattern of DOCS_ONLY_ROOT_FILES) {
|
|
209
|
+
if (path.startsWith(pattern))
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
if (codePaths && Array.isArray(codePaths) && codePaths.length > 0) {
|
|
215
|
+
const allDocsOnly = codePaths.every((p) => typeof p === 'string' && isDocsPath(p));
|
|
216
|
+
if (allDocsOnly) {
|
|
217
|
+
return { valid: true, errors: [] };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Validation failed - provide clear error message
|
|
221
|
+
const currentExposure = exposure || 'not set';
|
|
222
|
+
const currentType = type || 'not set';
|
|
223
|
+
return {
|
|
224
|
+
valid: false,
|
|
225
|
+
errors: [
|
|
226
|
+
`--docs-only flag used on ${wuId} but WU is not documentation-focused.\n\n` +
|
|
227
|
+
`Current exposure: ${currentExposure}\n` +
|
|
228
|
+
`Current type: ${currentType}\n\n` +
|
|
229
|
+
`--docs-only requires one of:\n` +
|
|
230
|
+
` 1. exposure: documentation\n` +
|
|
231
|
+
` 2. type: documentation\n` +
|
|
232
|
+
` 3. All code_paths under docs/, ai/, .claude/, or *.md files\n\n` +
|
|
233
|
+
`To fix, either:\n` +
|
|
234
|
+
` - Remove --docs-only flag and run full gates\n` +
|
|
235
|
+
` - Change WU exposure to 'documentation' if this is truly a docs-only change`,
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export function buildGatesCommand(options) {
|
|
240
|
+
const { docsOnly = false, isDocsOnly = false } = options;
|
|
241
|
+
// Use docs-only gates if either explicit flag or auto-detected
|
|
242
|
+
const shouldUseDocsOnly = docsOnly || isDocsOnly;
|
|
243
|
+
if (shouldUseDocsOnly) {
|
|
244
|
+
return `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`;
|
|
245
|
+
}
|
|
246
|
+
return `${PKG_MANAGER} ${SCRIPTS.GATES}`;
|
|
247
|
+
}
|
|
177
248
|
async function assertWorktreeWUInProgressInStateStore(id, worktreePath) {
|
|
178
249
|
const resolvedWorktreePath = path.resolve(worktreePath);
|
|
179
250
|
const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
|
|
@@ -810,15 +881,27 @@ function checkNodeModulesStaleness(worktreePath) {
|
|
|
810
881
|
console.warn(`${LOG_PREFIX.DONE} Could not check node_modules staleness: ${e.message}`);
|
|
811
882
|
}
|
|
812
883
|
}
|
|
813
|
-
|
|
884
|
+
/**
|
|
885
|
+
* Run gates in worktree
|
|
886
|
+
* @param {string} worktreePath - Path to worktree
|
|
887
|
+
* @param {string} id - WU ID
|
|
888
|
+
* @param {object} options - Gates options
|
|
889
|
+
* @param {boolean} options.isDocsOnly - Auto-detected docs-only from code_paths
|
|
890
|
+
* @param {boolean} options.docsOnly - Explicit --docs-only flag from CLI
|
|
891
|
+
*/
|
|
892
|
+
function runGatesInWorktree(worktreePath, id, options = {}) {
|
|
893
|
+
const { isDocsOnly = false, docsOnly = false } = options;
|
|
814
894
|
console.log(`\n${LOG_PREFIX.DONE} Running gates in worktree: ${worktreePath}`);
|
|
815
895
|
// Check for stale node_modules before running gates (prevents confusing failures)
|
|
816
896
|
checkNodeModulesStaleness(worktreePath);
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (
|
|
897
|
+
// WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
|
|
898
|
+
const useDocsOnlyGates = docsOnly || isDocsOnly;
|
|
899
|
+
const gatesCmd = buildGatesCommand({ docsOnly, isDocsOnly });
|
|
900
|
+
if (useDocsOnlyGates) {
|
|
821
901
|
console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
|
|
902
|
+
if (docsOnly) {
|
|
903
|
+
console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
|
|
904
|
+
}
|
|
822
905
|
}
|
|
823
906
|
const startTime = Date.now();
|
|
824
907
|
try {
|
|
@@ -1544,6 +1627,11 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
1544
1627
|
console.warn(`\nThis is a NON-BLOCKING warning.`);
|
|
1545
1628
|
console.warn(`Use --require-agents to make this a blocking error.\n`);
|
|
1546
1629
|
}
|
|
1630
|
+
// WU-1012: Validate --docs-only flag usage (BLOCKING)
|
|
1631
|
+
const docsOnlyValidation = validateDocsOnlyFlag(docForValidation, { docsOnly: args.docsOnly });
|
|
1632
|
+
if (!docsOnlyValidation.valid) {
|
|
1633
|
+
die(docsOnlyValidation.errors[0]);
|
|
1634
|
+
}
|
|
1547
1635
|
// WU-1999: Exposure validation (NON-BLOCKING warning)
|
|
1548
1636
|
printExposureWarnings(docForValidation, { skipExposureCheck: args.skipExposureCheck });
|
|
1549
1637
|
// WU-2022: Feature accessibility validation (BLOCKING)
|
|
@@ -1656,11 +1744,14 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
|
|
|
1656
1744
|
else if (isBranchOnly) {
|
|
1657
1745
|
// Branch-Only mode: run gates in-place (current directory on lane branch)
|
|
1658
1746
|
console.log(`\n${LOG_PREFIX.DONE} Running gates in Branch-Only mode (in-place on lane branch)`);
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
if (
|
|
1747
|
+
// WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
|
|
1748
|
+
const useDocsOnlyGates = args.docsOnly || isDocsOnly;
|
|
1749
|
+
const gatesCmd = buildGatesCommand({ docsOnly: Boolean(args.docsOnly), isDocsOnly });
|
|
1750
|
+
if (useDocsOnlyGates) {
|
|
1663
1751
|
console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
|
|
1752
|
+
if (args.docsOnly) {
|
|
1753
|
+
console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
|
|
1754
|
+
}
|
|
1664
1755
|
}
|
|
1665
1756
|
const startTime = Date.now();
|
|
1666
1757
|
try {
|
|
@@ -1697,7 +1788,8 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
|
|
|
1697
1788
|
}
|
|
1698
1789
|
else if (worktreePath && existsSync(worktreePath)) {
|
|
1699
1790
|
// Worktree mode: run gates in the dedicated worktree
|
|
1700
|
-
|
|
1791
|
+
// WU-1012: Pass both auto-detected and explicit docs-only flags
|
|
1792
|
+
runGatesInWorktree(worktreePath, id, { isDocsOnly, docsOnly: Boolean(args.docsOnly) });
|
|
1701
1793
|
}
|
|
1702
1794
|
else {
|
|
1703
1795
|
die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
|