@lumenflow/cli 1.0.0 → 1.3.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 +126 -49
- package/dist/init.js +297 -0
- package/dist/initiative-bulk-assign-wus.js +315 -0
- package/dist/initiative-create.js +3 -7
- package/dist/initiative-edit.js +3 -3
- 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 +313 -116
- package/dist/wu-cleanup.js +49 -3
- package/dist/wu-create.js +195 -121
- package/dist/wu-delete.js +241 -0
- package/dist/wu-done.js +146 -23
- package/dist/wu-edit.js +152 -61
- package/dist/wu-infer-lane.js +2 -2
- package/dist/wu-spawn.js +77 -158
- package/dist/wu-unlock-lane.js +158 -0
- package/package.json +30 -10
|
@@ -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
|
@@ -147,7 +147,7 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
|
|
|
147
147
|
` pnpm wu:repair-claim --id ${id}\n\n` +
|
|
148
148
|
`After repair, retry:\n` +
|
|
149
149
|
` pnpm wu:done --id ${id}\n\n` +
|
|
150
|
-
`See:
|
|
150
|
+
`See: docs/04-operations/_frameworks/lumenflow/agent/onboarding/troubleshooting-wu-done.md for more recovery options.`);
|
|
151
151
|
}
|
|
152
152
|
export function printExposureWarnings(wu, options = {}) {
|
|
153
153
|
// Validate exposure
|
|
@@ -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,11 +1788,13 @@ 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` +
|
|
1704
|
-
`If the worktree was removed, recreate it and retry, or
|
|
1796
|
+
`If the worktree was removed, recreate it and retry, or rerun with --branch-only when the lane branch exists.\n` +
|
|
1797
|
+
`Use --skip-gates only with justification.`);
|
|
1705
1798
|
}
|
|
1706
1799
|
// Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
|
|
1707
1800
|
if (!args.skipCosGates) {
|
|
@@ -1766,6 +1859,13 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
|
|
|
1766
1859
|
* @param {string|null} params.derivedWorktree - Derived worktree path
|
|
1767
1860
|
* @param {string} params.STAMPS_DIR - Stamps directory path
|
|
1768
1861
|
*/
|
|
1862
|
+
export function computeBranchOnlyFallback({ isBranchOnly, branchOnlyRequested, worktreeExists, derivedWorktree, }) {
|
|
1863
|
+
const allowFallback = Boolean(branchOnlyRequested) && !isBranchOnly && !worktreeExists && Boolean(derivedWorktree);
|
|
1864
|
+
return {
|
|
1865
|
+
allowFallback,
|
|
1866
|
+
effectiveBranchOnly: isBranchOnly || allowFallback,
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1769
1869
|
function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR }) {
|
|
1770
1870
|
const stampExists = existsSync(path.join(STAMPS_DIR, `${id}.done`)) ? 'yes' : 'no';
|
|
1771
1871
|
const yamlStatus = docMain.status || 'unknown';
|
|
@@ -1777,6 +1877,8 @@ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree,
|
|
|
1777
1877
|
}
|
|
1778
1878
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
1779
1879
|
async function main() {
|
|
1880
|
+
// Allow pre-push hook to recognize wu:done automation (WU-1030)
|
|
1881
|
+
process.env.LUMENFLOW_WU_TOOL = 'wu-done';
|
|
1780
1882
|
// Validate CLI arguments and WU ID format (extracted to wu-done-validators.mjs)
|
|
1781
1883
|
const { args, id } = validateInputs(process.argv);
|
|
1782
1884
|
// Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
|
|
@@ -1784,25 +1886,39 @@ async function main() {
|
|
|
1784
1886
|
const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain, isBranchOnly, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
|
|
1785
1887
|
// Capture main checkout path once. process.cwd() may drift later during recovery flows.
|
|
1786
1888
|
const mainCheckoutPath = process.cwd();
|
|
1889
|
+
// Resolve worktree path early so we can detect missing worktree before pre-flight checks
|
|
1890
|
+
const resolvedWorktreePath = derivedWorktree && !isBranchOnly
|
|
1891
|
+
? path.isAbsolute(derivedWorktree)
|
|
1892
|
+
? derivedWorktree
|
|
1893
|
+
: path.resolve(mainCheckoutPath, derivedWorktree)
|
|
1894
|
+
: null;
|
|
1895
|
+
const worktreeExists = resolvedWorktreePath ? existsSync(resolvedWorktreePath) : false;
|
|
1896
|
+
const { allowFallback: allowBranchOnlyFallback, effectiveBranchOnly } = computeBranchOnlyFallback({
|
|
1897
|
+
isBranchOnly,
|
|
1898
|
+
branchOnlyRequested: args.branchOnly,
|
|
1899
|
+
worktreeExists,
|
|
1900
|
+
derivedWorktree,
|
|
1901
|
+
});
|
|
1902
|
+
if (allowBranchOnlyFallback) {
|
|
1903
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree missing (${resolvedWorktreePath}). Proceeding in branch-only mode because --branch-only was provided.`);
|
|
1904
|
+
}
|
|
1905
|
+
const effectiveDerivedWorktree = effectiveBranchOnly ? null : derivedWorktree;
|
|
1906
|
+
const effectiveWorktreePath = effectiveBranchOnly ? null : resolvedWorktreePath;
|
|
1787
1907
|
// Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
|
|
1788
1908
|
const preFlightResult = await executePreFlightChecks({
|
|
1789
1909
|
id,
|
|
1790
1910
|
args,
|
|
1791
|
-
isBranchOnly,
|
|
1911
|
+
isBranchOnly: effectiveBranchOnly,
|
|
1792
1912
|
isDocsOnly,
|
|
1793
1913
|
docMain,
|
|
1794
1914
|
docForValidation: initialDocForValidation,
|
|
1795
|
-
derivedWorktree,
|
|
1915
|
+
derivedWorktree: effectiveDerivedWorktree,
|
|
1796
1916
|
});
|
|
1797
1917
|
const title = preFlightResult.title;
|
|
1798
1918
|
// Note: docForValidation is returned but not used after pre-flight checks
|
|
1799
1919
|
// The metadata transaction uses docForUpdate instead
|
|
1800
1920
|
// Step 0: Run gates (WU-1215: extracted to executeGates function)
|
|
1801
|
-
const worktreePath =
|
|
1802
|
-
? path.isAbsolute(derivedWorktree)
|
|
1803
|
-
? derivedWorktree
|
|
1804
|
-
: path.resolve(mainCheckoutPath, derivedWorktree)
|
|
1805
|
-
: null;
|
|
1921
|
+
const worktreePath = effectiveWorktreePath;
|
|
1806
1922
|
// WU-1943: Check if any checkpoints exist for this WU session
|
|
1807
1923
|
// Warn (don't block) if no checkpoints - agent should have been checkpointing periodically
|
|
1808
1924
|
try {
|
|
@@ -1816,9 +1932,16 @@ async function main() {
|
|
|
1816
1932
|
catch {
|
|
1817
1933
|
// Non-blocking: checkpoint check failure should not block wu:done
|
|
1818
1934
|
}
|
|
1819
|
-
await executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath });
|
|
1935
|
+
await executeGates({ id, args, isBranchOnly: effectiveBranchOnly, isDocsOnly, worktreePath });
|
|
1820
1936
|
// Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
|
|
1821
|
-
printStateHUD({
|
|
1937
|
+
printStateHUD({
|
|
1938
|
+
id,
|
|
1939
|
+
docMain,
|
|
1940
|
+
isBranchOnly: effectiveBranchOnly,
|
|
1941
|
+
isDocsOnly,
|
|
1942
|
+
derivedWorktree: effectiveDerivedWorktree,
|
|
1943
|
+
STAMPS_DIR,
|
|
1944
|
+
});
|
|
1822
1945
|
// Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
|
|
1823
1946
|
// This prevents partial completion states where merge succeeds but commit fails
|
|
1824
1947
|
// Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
|
|
@@ -1855,7 +1978,7 @@ async function main() {
|
|
|
1855
1978
|
validateStagedFiles,
|
|
1856
1979
|
};
|
|
1857
1980
|
try {
|
|
1858
|
-
if (
|
|
1981
|
+
if (effectiveBranchOnly) {
|
|
1859
1982
|
// Branch-Only mode: merge first, then update metadata on main
|
|
1860
1983
|
// NOTE: Branch-only still uses old rollback mechanism
|
|
1861
1984
|
const branchOnlyContext = {
|
|
@@ -2073,7 +2196,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
|
|
|
2073
2196
|
// Get files changed in this branch vs base
|
|
2074
2197
|
const diff = await git.raw(['diff', '--name-only', baseBranch]);
|
|
2075
2198
|
const changedFiles = diff.split('\n').filter(Boolean);
|
|
2076
|
-
// Filter to docs:
|
|
2199
|
+
// Filter to docs: docs/04-operations/_frameworks/lumenflow/agent/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
|
|
2077
2200
|
const docPatterns = [
|
|
2078
2201
|
/^ai\/onboarding\//,
|
|
2079
2202
|
/^docs\//,
|