@lumenflow/cli 1.1.0 → 1.3.2
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__/cli-entry-point.test.js +50 -0
- package/dist/__tests__/cli-subprocess.test.js +64 -0
- package/dist/cli-entry-point.js +46 -0
- package/dist/gates.js +102 -39
- package/dist/init.js +241 -195
- package/dist/initiative-add-wu.js +2 -1
- package/dist/initiative-create.js +5 -8
- package/dist/initiative-edit.js +3 -3
- package/dist/initiative-list.js +2 -1
- package/dist/initiative-status.js +2 -1
- package/dist/wu-claim.js +297 -110
- package/dist/wu-cleanup.js +129 -57
- package/dist/wu-create.js +197 -122
- package/dist/wu-deps.js +2 -1
- package/dist/wu-done.js +46 -14
- package/dist/wu-edit.js +152 -61
- package/dist/wu-infer-lane.js +5 -4
- package/dist/wu-preflight.js +2 -1
- package/dist/wu-prune.js +12 -3
- package/dist/wu-repair.js +2 -1
- package/dist/wu-spawn.js +79 -159
- package/dist/wu-unlock-lane.js +6 -1
- package/dist/wu-validate.js +2 -1
- package/package.json +14 -14
- package/dist/gates.d.ts +0 -41
- package/dist/gates.d.ts.map +0 -1
- package/dist/gates.js.map +0 -1
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-add-wu.d.ts.map +0 -1
- package/dist/initiative-add-wu.js.map +0 -1
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-create.d.ts.map +0 -1
- package/dist/initiative-create.js.map +0 -1
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-edit.d.ts.map +0 -1
- package/dist/initiative-edit.js.map +0 -1
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-list.d.ts.map +0 -1
- package/dist/initiative-list.js.map +0 -1
- package/dist/initiative-status.d.ts +0 -11
- package/dist/initiative-status.d.ts.map +0 -1
- package/dist/initiative-status.js.map +0 -1
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-checkpoint.d.ts.map +0 -1
- package/dist/mem-checkpoint.js.map +0 -1
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-cleanup.d.ts.map +0 -1
- package/dist/mem-cleanup.js.map +0 -1
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-create.d.ts.map +0 -1
- package/dist/mem-create.js.map +0 -1
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-inbox.d.ts.map +0 -1
- package/dist/mem-inbox.js.map +0 -1
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-init.d.ts.map +0 -1
- package/dist/mem-init.js.map +0 -1
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-ready.d.ts.map +0 -1
- package/dist/mem-ready.js.map +0 -1
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-signal.d.ts.map +0 -1
- package/dist/mem-signal.js.map +0 -1
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-start.d.ts.map +0 -1
- package/dist/mem-start.js.map +0 -1
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-summarize.d.ts.map +0 -1
- package/dist/mem-summarize.js.map +0 -1
- package/dist/mem-triage.d.ts +0 -22
- package/dist/mem-triage.d.ts.map +0 -1
- package/dist/mem-triage.js.map +0 -1
- package/dist/spawn-list.d.ts +0 -16
- package/dist/spawn-list.d.ts.map +0 -1
- package/dist/spawn-list.js.map +0 -1
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-block.d.ts.map +0 -1
- package/dist/wu-block.js.map +0 -1
- package/dist/wu-claim.d.ts +0 -32
- package/dist/wu-claim.d.ts.map +0 -1
- package/dist/wu-claim.js.map +0 -1
- package/dist/wu-cleanup.d.ts +0 -17
- package/dist/wu-cleanup.d.ts.map +0 -1
- package/dist/wu-cleanup.js.map +0 -1
- package/dist/wu-create.d.ts +0 -38
- package/dist/wu-create.d.ts.map +0 -1
- package/dist/wu-create.js.map +0 -1
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-deps.d.ts.map +0 -1
- package/dist/wu-deps.js.map +0 -1
- package/dist/wu-done.d.ts +0 -153
- package/dist/wu-done.d.ts.map +0 -1
- package/dist/wu-done.js.map +0 -1
- package/dist/wu-edit.d.ts +0 -29
- package/dist/wu-edit.d.ts.map +0 -1
- package/dist/wu-edit.js.map +0 -1
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-infer-lane.d.ts.map +0 -1
- package/dist/wu-infer-lane.js.map +0 -1
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-preflight.d.ts.map +0 -1
- package/dist/wu-preflight.js.map +0 -1
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-prune.d.ts.map +0 -1
- package/dist/wu-prune.js.map +0 -1
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-repair.d.ts.map +0 -1
- package/dist/wu-repair.js.map +0 -1
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -168
- package/dist/wu-spawn.d.ts.map +0 -1
- package/dist/wu-spawn.js.map +0 -1
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unblock.d.ts.map +0 -1
- package/dist/wu-unblock.js.map +0 -1
- package/dist/wu-validate.d.ts +0 -16
- package/dist/wu-validate.d.ts.map +0 -1
- package/dist/wu-validate.js.map +0 -1
package/dist/wu-cleanup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Cleans up worktree and branch after PR merge (PR-based completion workflow).
|
|
6
6
|
*
|
|
7
7
|
* Sequence:
|
|
8
|
-
* 1) Verify PR is merged (via gh API
|
|
8
|
+
* 1) Verify PR is merged (via gh API; no merge-base fallback)
|
|
9
9
|
* 2) Remove worktree (if exists)
|
|
10
10
|
* 3) Delete lane branch (local + remote)
|
|
11
11
|
*
|
|
@@ -13,33 +13,60 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
15
|
* pnpm wu:cleanup --id WU-703
|
|
16
|
+
* pnpm wu:cleanup --artifacts
|
|
16
17
|
*/
|
|
17
18
|
import { execSync } from 'node:child_process';
|
|
18
|
-
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
19
|
+
import { createGitForPath, getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
19
20
|
import { existsSync } from 'node:fs';
|
|
20
21
|
import path from 'node:path';
|
|
21
22
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
23
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
24
|
+
import { cleanupWorktreeBuildArtifacts } from '@lumenflow/core/dist/rebase-artifact-cleanup.js';
|
|
25
|
+
import { detectCurrentWorktree } from '@lumenflow/core/dist/wu-done-validators.js';
|
|
22
26
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
23
27
|
import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
24
|
-
import {
|
|
28
|
+
import { isGhCliAvailable } from '@lumenflow/core/dist/wu-done-pr.js';
|
|
29
|
+
import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
25
30
|
// WU-2278: Import ownership validation for cross-agent protection
|
|
26
31
|
import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
|
|
27
32
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const CLEANUP_OPTIONS = {
|
|
34
|
+
artifacts: {
|
|
35
|
+
name: 'artifacts',
|
|
36
|
+
flags: '--artifacts',
|
|
37
|
+
description: 'Remove build artifacts (dist, tsbuildinfo) in current worktree',
|
|
38
|
+
},
|
|
33
39
|
};
|
|
40
|
+
export const CLEANUP_GUARD_REASONS = CLEANUP_GUARD.REASONS;
|
|
41
|
+
export function evaluateCleanupGuards({ hasUncommittedChanges, hasUnpushedCommits, hasStamp, yamlStatus, ghAvailable, prMerged, }) {
|
|
42
|
+
if (hasUncommittedChanges) {
|
|
43
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNCOMMITTED_CHANGES };
|
|
44
|
+
}
|
|
45
|
+
if (hasUnpushedCommits) {
|
|
46
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNPUSHED_COMMITS };
|
|
47
|
+
}
|
|
48
|
+
if (yamlStatus !== WU_STATUS.DONE) {
|
|
49
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.STATUS_NOT_DONE };
|
|
50
|
+
}
|
|
51
|
+
if (!hasStamp) {
|
|
52
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.MISSING_STAMP };
|
|
53
|
+
}
|
|
54
|
+
if (ghAvailable && prMerged !== true) {
|
|
55
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.PR_NOT_MERGED };
|
|
56
|
+
}
|
|
57
|
+
return { allowed: true, reason: null };
|
|
58
|
+
}
|
|
34
59
|
// Help text is now auto-generated by commander via createWUParser
|
|
35
60
|
async function verifyPRMerged(laneBranch) {
|
|
36
|
-
|
|
61
|
+
if (!isGhCliAvailable()) {
|
|
62
|
+
return { merged: null, method: 'gh_unavailable' };
|
|
63
|
+
}
|
|
37
64
|
let ghResult;
|
|
38
65
|
try {
|
|
39
66
|
ghResult = execSync(`gh api repos/:owner/:repo/pulls -q '.[] | select(.head.ref == "${laneBranch}") | .merged'`, { encoding: FILE_SYSTEM.UTF8 }).trim();
|
|
40
67
|
}
|
|
41
68
|
catch {
|
|
42
|
-
ghResult =
|
|
69
|
+
ghResult = STRING_LITERALS.EMPTY;
|
|
43
70
|
}
|
|
44
71
|
if (ghResult === 'true') {
|
|
45
72
|
return { merged: true, method: 'gh_api' };
|
|
@@ -47,37 +74,7 @@ async function verifyPRMerged(laneBranch) {
|
|
|
47
74
|
if (ghResult === 'false') {
|
|
48
75
|
return { merged: false, method: 'gh_api' };
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
// Always fetch origin/main first for accurate merge-base check
|
|
52
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
53
|
-
const localBranchExists = await getGitForCwd().branchExists(laneBranch);
|
|
54
|
-
if (!localBranchExists) {
|
|
55
|
-
// Branch doesn't exist locally - check if it exists remotely
|
|
56
|
-
const remoteBranchExists = await getGitForCwd().raw([
|
|
57
|
-
'ls-remote',
|
|
58
|
-
'--heads',
|
|
59
|
-
REMOTES.ORIGIN,
|
|
60
|
-
laneBranch,
|
|
61
|
-
]);
|
|
62
|
-
if (!remoteBranchExists) {
|
|
63
|
-
// Branch is gone both locally and remotely - assume merged
|
|
64
|
-
return { merged: true, method: 'branch_deleted' };
|
|
65
|
-
}
|
|
66
|
-
// Branch exists remotely but not locally - need to fetch
|
|
67
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, laneBranch);
|
|
68
|
-
}
|
|
69
|
-
let isAncestor;
|
|
70
|
-
try {
|
|
71
|
-
await getGitForCwd().raw(['merge-base', '--is-ancestor', laneBranch, GIT_REFS.ORIGIN_MAIN]);
|
|
72
|
-
isAncestor = true;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
isAncestor = false;
|
|
76
|
-
}
|
|
77
|
-
if (isAncestor) {
|
|
78
|
-
return { merged: true, method: 'git_merge_base' };
|
|
79
|
-
}
|
|
80
|
-
return { merged: false, method: 'git_merge_base' };
|
|
77
|
+
return { merged: null, method: 'gh_api' };
|
|
81
78
|
}
|
|
82
79
|
async function removeWorktree(worktreePath) {
|
|
83
80
|
if (!existsSync(worktreePath)) {
|
|
@@ -115,14 +112,74 @@ async function deleteBranch(laneBranch) {
|
|
|
115
112
|
console.log(`[wu-cleanup] ✓ Remote branch already deleted: ${laneBranch}`);
|
|
116
113
|
}
|
|
117
114
|
}
|
|
115
|
+
async function cleanupArtifactsInWorktree() {
|
|
116
|
+
const worktreePath = detectCurrentWorktree();
|
|
117
|
+
if (!worktreePath) {
|
|
118
|
+
die(`${LOG_PREFIX.CLEANUP} Not in a worktree.\n\n` +
|
|
119
|
+
'Run this command from inside a worktree:\n' +
|
|
120
|
+
' cd worktrees/<lane>-wu-xxx\n' +
|
|
121
|
+
' pnpm wu:cleanup --artifacts\n');
|
|
122
|
+
}
|
|
123
|
+
console.log(`${LOG_PREFIX.CLEANUP} Cleaning build artifacts in ${worktreePath}`);
|
|
124
|
+
const result = await cleanupWorktreeBuildArtifacts(worktreePath);
|
|
125
|
+
if (result.removedCount === 0) {
|
|
126
|
+
console.log(`${LOG_PREFIX.CLEANUP} ✓ No build artifacts found`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (result.distDirectories.length > 0) {
|
|
130
|
+
console.log(`${LOG_PREFIX.CLEANUP} Removed dist directories:`);
|
|
131
|
+
for (const dir of result.distDirectories) {
|
|
132
|
+
console.log(` - ${dir}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (result.tsbuildinfoFiles.length > 0) {
|
|
136
|
+
console.log(`${LOG_PREFIX.CLEANUP} Removed tsbuildinfo files:`);
|
|
137
|
+
for (const file of result.tsbuildinfoFiles) {
|
|
138
|
+
console.log(` - ${file}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log(`${LOG_PREFIX.CLEANUP} ✓ Build artifact cleanup complete`);
|
|
142
|
+
}
|
|
143
|
+
async function hasUncommittedChanges(worktreePath) {
|
|
144
|
+
if (!existsSync(worktreePath)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const git = createGitForPath(worktreePath);
|
|
148
|
+
const status = await git.getStatus();
|
|
149
|
+
return status.length > 0;
|
|
150
|
+
}
|
|
151
|
+
async function hasUnpushedCommits(worktreePath) {
|
|
152
|
+
if (!existsSync(worktreePath)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const git = createGitForPath(worktreePath);
|
|
156
|
+
try {
|
|
157
|
+
const unpushed = await git.getUnpushedCommits();
|
|
158
|
+
return unpushed.length > 0;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function hasStampFile(wuId) {
|
|
165
|
+
const stampPath = path.join(process.cwd(), WU_PATHS.STAMP(wuId));
|
|
166
|
+
return existsSync(stampPath);
|
|
167
|
+
}
|
|
118
168
|
async function main() {
|
|
119
169
|
const args = createWUParser({
|
|
120
170
|
name: 'wu-cleanup',
|
|
121
171
|
description: 'Clean up worktree and branch after PR merge (PR-based completion workflow)',
|
|
122
|
-
options: [WU_OPTIONS.id],
|
|
123
|
-
required: [
|
|
172
|
+
options: [WU_OPTIONS.id, CLEANUP_OPTIONS.artifacts],
|
|
173
|
+
required: [],
|
|
124
174
|
allowPositionalId: true,
|
|
125
175
|
});
|
|
176
|
+
if (args.artifacts) {
|
|
177
|
+
await cleanupArtifactsInWorktree();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!args.id) {
|
|
181
|
+
die(`${LOG_PREFIX.CLEANUP} Missing required --id (or use --artifacts).`);
|
|
182
|
+
}
|
|
126
183
|
const id = args.id.toUpperCase();
|
|
127
184
|
const wu = readWU(WU_PATHS.WU(id), id);
|
|
128
185
|
// Use kebab-case lane naming (match wu-claim.mjs logic)
|
|
@@ -135,6 +192,7 @@ async function main() {
|
|
|
135
192
|
const idK = args.id.toLowerCase();
|
|
136
193
|
const laneBranch = `lane/${laneK}/${idK}`;
|
|
137
194
|
const worktreePath = path.join('worktrees', `${laneK}-${idK}`);
|
|
195
|
+
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
138
196
|
console.log(`[wu-cleanup] Cleaning up ${args.id} (${wu.title})`);
|
|
139
197
|
console.log(`[wu-cleanup] Lane: ${wu.lane}`);
|
|
140
198
|
console.log(`[wu-cleanup] Branch: ${laneBranch}`);
|
|
@@ -154,25 +212,38 @@ async function main() {
|
|
|
154
212
|
console.error(BOX.BOT);
|
|
155
213
|
process.exit(EXIT_CODES.ERROR);
|
|
156
214
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
const cleanupCheck = {
|
|
216
|
+
hasUncommittedChanges: await hasUncommittedChanges(absoluteWorktreePath),
|
|
217
|
+
hasUnpushedCommits: await hasUnpushedCommits(absoluteWorktreePath),
|
|
218
|
+
hasStamp: hasStampFile(id),
|
|
219
|
+
yamlStatus: wu.status,
|
|
220
|
+
ghAvailable: isGhCliAvailable(),
|
|
221
|
+
prMerged: null,
|
|
222
|
+
};
|
|
223
|
+
if (cleanupCheck.ghAvailable) {
|
|
224
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.START}`);
|
|
225
|
+
const { merged, method } = await verifyPRMerged(laneBranch);
|
|
226
|
+
cleanupCheck.prMerged = merged;
|
|
227
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.RESULT} ${method}`);
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
const guardResult = evaluateCleanupGuards(cleanupCheck);
|
|
231
|
+
if (!guardResult.allowed) {
|
|
161
232
|
console.error();
|
|
162
233
|
console.error(BOX.TOP);
|
|
163
|
-
console.error(
|
|
234
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.BLOCKED}`);
|
|
164
235
|
console.error(BOX.MID);
|
|
165
|
-
console.error(
|
|
166
|
-
console.error(
|
|
167
|
-
console.error(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.MESSAGES[guardResult.reason]}`);
|
|
237
|
+
console.error(`${BOX.SIDE}`);
|
|
238
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.NEXT_STEPS}`);
|
|
239
|
+
const steps = CLEANUP_GUARD.NEXT_STEPS[guardResult.reason] || CLEANUP_GUARD.NEXT_STEPS.DEFAULT;
|
|
240
|
+
for (const step of steps) {
|
|
241
|
+
const line = step.appendId ? `${step.text} ${args.id}` : step.text;
|
|
242
|
+
console.error(`${BOX.SIDE} ${line}`);
|
|
243
|
+
}
|
|
171
244
|
console.error(BOX.BOT);
|
|
172
245
|
process.exit(EXIT_CODES.ERROR);
|
|
173
246
|
}
|
|
174
|
-
console.log(`[wu-cleanup] ✓ PR merged (verified via ${method})`);
|
|
175
|
-
console.log();
|
|
176
247
|
// 2. Remove worktree
|
|
177
248
|
await removeWorktree(worktreePath);
|
|
178
249
|
console.log();
|
|
@@ -189,6 +260,7 @@ async function main() {
|
|
|
189
260
|
}
|
|
190
261
|
// Guard main() for testability (WU-1366)
|
|
191
262
|
import { fileURLToPath } from 'node:url';
|
|
263
|
+
import { runCLI } from './cli-entry-point.js';
|
|
192
264
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
193
|
-
main
|
|
265
|
+
runCLI(main);
|
|
194
266
|
}
|
package/dist/wu-create.js
CHANGED
|
@@ -40,7 +40,7 @@ import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
|
|
|
40
40
|
import { parseBacklogFrontmatter } from '@lumenflow/core/dist/backlog-parser.js';
|
|
41
41
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
42
42
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
43
|
-
import {
|
|
43
|
+
import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
|
|
44
44
|
import { COMMIT_FORMATS, FILE_SYSTEM, STRING_LITERALS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
|
|
45
45
|
// WU-1593: Use centralized validateWUIDFormat (DRY)
|
|
46
46
|
import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/dist/wu-helpers.js';
|
|
@@ -52,6 +52,8 @@ import { validateSpecCompleteness } from '@lumenflow/core/dist/wu-done-validator
|
|
|
52
52
|
import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
53
53
|
// WU-2253: Import WU spec linter for acceptance/code_paths validation
|
|
54
54
|
import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
|
|
55
|
+
// WU-1025: Import placeholder validator for inline content validation
|
|
56
|
+
import { validateNoPlaceholders, buildPlaceholderErrorMessage, } from '@lumenflow/core/dist/wu-validator.js';
|
|
55
57
|
/** Log prefix for console output */
|
|
56
58
|
const LOG_PREFIX = '[wu:create]';
|
|
57
59
|
/** Micro-worktree operation name */
|
|
@@ -175,67 +177,29 @@ function displayReadinessSummary(id) {
|
|
|
175
177
|
console.warn(`${LOG_PREFIX} ⚠️ Could not validate readiness: ${err.message}`);
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
* @param {Object} opts - Additional options
|
|
188
|
-
* @returns {string} Relative path to created YAML file
|
|
189
|
-
*/
|
|
190
|
-
function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, opts = {}) {
|
|
191
|
-
const wuRelativePath = WU_PATHS.WU(id);
|
|
192
|
-
const wuAbsolutePath = join(worktreePath, wuRelativePath);
|
|
193
|
-
const wuDir = join(worktreePath, WU_PATHS.WU_DIR());
|
|
194
|
-
mkdirSync(wuDir, { recursive: true });
|
|
195
|
-
// WU-1428: Use todayISO() for consistent YYYY-MM-DD format (library-first)
|
|
196
|
-
const today = todayISO();
|
|
197
|
-
// Parse initiative system fields from opts (WU-1247) and assigned_to (WU-1368)
|
|
198
|
-
const { initiative, phase, blockedBy, blocks, labels, assignedTo } = opts;
|
|
199
|
-
// WU-1364: Parse full spec inline options
|
|
200
|
-
const { description: inlineDescription, acceptance: inlineAcceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, } = opts;
|
|
201
|
-
// WU-1998: Parse exposure field options
|
|
202
|
-
const { exposure, userJourney, uiPairingWus } = opts;
|
|
203
|
-
// WU-2320: Parse spec_refs option
|
|
204
|
-
const { specRefs } = opts;
|
|
205
|
-
// Helper to parse comma-separated strings into arrays (DRY)
|
|
206
|
-
const parseCommaSeparated = (value) => value
|
|
207
|
-
? value
|
|
208
|
-
.split(',')
|
|
209
|
-
.map((s) => s.trim())
|
|
210
|
-
.filter(Boolean)
|
|
211
|
-
: [];
|
|
212
|
-
// WU-1364: Build description (inline or placeholder)
|
|
213
|
-
const description = inlineDescription
|
|
214
|
-
? inlineDescription
|
|
215
|
-
: `${PLACEHOLDER_SENTINEL} Describe the work to be done.\n\nContext: ...\nProblem: ...\nSolution: ...\n`;
|
|
216
|
-
// WU-1364: Build acceptance (inline array or placeholder)
|
|
217
|
-
const acceptance = inlineAcceptance && inlineAcceptance.length > 0
|
|
218
|
-
? inlineAcceptance
|
|
219
|
-
: [
|
|
220
|
-
`${PLACEHOLDER_SENTINEL} Define acceptance criteria`,
|
|
221
|
-
'pnpm format, lint, typecheck → PASS',
|
|
222
|
-
];
|
|
223
|
-
// WU-1364: Build code_paths from inline flag
|
|
180
|
+
// Helper to parse comma-separated strings into arrays (DRY)
|
|
181
|
+
const parseCommaSeparated = (value) => value
|
|
182
|
+
? value
|
|
183
|
+
.split(',')
|
|
184
|
+
.map((s) => s.trim())
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
: [];
|
|
187
|
+
function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
|
|
188
|
+
const { description, acceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
|
|
224
189
|
const code_paths = parseCommaSeparated(codePaths);
|
|
225
|
-
// WU-1364: Build tests object from inline flags
|
|
226
190
|
const tests = {
|
|
227
191
|
manual: parseCommaSeparated(testPathsManual),
|
|
228
192
|
unit: parseCommaSeparated(testPathsUnit),
|
|
229
193
|
e2e: parseCommaSeparated(testPathsE2e),
|
|
230
194
|
};
|
|
231
|
-
|
|
195
|
+
return {
|
|
232
196
|
id,
|
|
233
197
|
title,
|
|
234
198
|
lane,
|
|
235
199
|
type,
|
|
236
200
|
status: 'ready',
|
|
237
201
|
priority,
|
|
238
|
-
created
|
|
202
|
+
created,
|
|
239
203
|
description,
|
|
240
204
|
acceptance,
|
|
241
205
|
code_paths,
|
|
@@ -245,24 +209,107 @@ function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, o
|
|
|
245
209
|
risks: [],
|
|
246
210
|
notes: '',
|
|
247
211
|
requires_review: false,
|
|
248
|
-
// Initiative system fields - only include if provided (WU-1247)
|
|
249
212
|
...(initiative && { initiative }),
|
|
250
213
|
...(phase && { phase: parseInt(phase, 10) }),
|
|
251
214
|
...(blockedBy && { blocked_by: blockedBy.split(',').map((s) => s.trim()) }),
|
|
252
215
|
...(blocks && { blocks: blocks.split(',').map((s) => s.trim()) }),
|
|
253
216
|
...(labels && { labels: labels.split(',').map((s) => s.trim()) }),
|
|
254
|
-
// WU-1368: Default assigned_to from git config user.email
|
|
255
217
|
...(assignedTo && { assigned_to: assignedTo }),
|
|
256
|
-
// WU-1998: Exposure field options - only include if provided
|
|
257
218
|
...(exposure && { exposure }),
|
|
258
219
|
...(userJourney && { user_journey: userJourney }),
|
|
259
220
|
...(uiPairingWus && { ui_pairing_wus: parseCommaSeparated(uiPairingWus) }),
|
|
260
|
-
// WU-2320: Spec references - only include if provided
|
|
261
221
|
...(specRefs && { spec_refs: parseCommaSeparated(specRefs) }),
|
|
262
222
|
};
|
|
263
|
-
|
|
223
|
+
}
|
|
224
|
+
export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
|
|
225
|
+
const errors = [];
|
|
226
|
+
const effectiveType = type || DEFAULT_TYPE;
|
|
227
|
+
if (!opts.description) {
|
|
228
|
+
errors.push('--description is required');
|
|
229
|
+
}
|
|
230
|
+
if (!opts.acceptance || opts.acceptance.length === 0) {
|
|
231
|
+
errors.push('--acceptance is required (repeatable)');
|
|
232
|
+
}
|
|
233
|
+
if (!opts.exposure) {
|
|
234
|
+
errors.push('--exposure is required');
|
|
235
|
+
}
|
|
236
|
+
const hasTestPaths = opts.testPathsManual || opts.testPathsUnit || opts.testPathsE2e;
|
|
237
|
+
if (effectiveType !== 'documentation' && effectiveType !== 'process') {
|
|
238
|
+
if (!opts.codePaths) {
|
|
239
|
+
errors.push('--code-paths is required for non-documentation WUs');
|
|
240
|
+
}
|
|
241
|
+
if (!hasTestPaths) {
|
|
242
|
+
errors.push('At least one test path flag is required (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (effectiveType === 'feature' && !opts.specRefs) {
|
|
246
|
+
errors.push('--spec-refs is required for type: feature WUs');
|
|
247
|
+
}
|
|
248
|
+
if (errors.length > 0) {
|
|
249
|
+
return { valid: false, errors };
|
|
250
|
+
}
|
|
251
|
+
const placeholderResult = validateNoPlaceholders({
|
|
252
|
+
description: opts.description,
|
|
253
|
+
acceptance: opts.acceptance,
|
|
254
|
+
});
|
|
255
|
+
if (!placeholderResult.valid) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
errors: [buildPlaceholderErrorMessage('wu:create', placeholderResult)],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const today = todayISO();
|
|
262
|
+
const wuContent = buildWUContent({
|
|
263
|
+
id,
|
|
264
|
+
lane,
|
|
265
|
+
title,
|
|
266
|
+
priority,
|
|
267
|
+
type: effectiveType,
|
|
268
|
+
created: today,
|
|
269
|
+
opts,
|
|
270
|
+
});
|
|
271
|
+
const schemaResult = validateWU(wuContent);
|
|
272
|
+
if (!schemaResult.success) {
|
|
273
|
+
const schemaErrors = schemaResult.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
|
|
274
|
+
return { valid: false, errors: schemaErrors };
|
|
275
|
+
}
|
|
276
|
+
const completeness = validateSpecCompleteness(wuContent, id);
|
|
277
|
+
if (!completeness.valid) {
|
|
278
|
+
return { valid: false, errors: completeness.errors };
|
|
279
|
+
}
|
|
280
|
+
return { valid: true, errors: [] };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Create WU YAML file in micro-worktree
|
|
284
|
+
*
|
|
285
|
+
* @param {string} worktreePath - Path to micro-worktree
|
|
286
|
+
* @param {string} id - WU ID
|
|
287
|
+
* @param {string} lane - WU lane
|
|
288
|
+
* @param {string} title - WU title
|
|
289
|
+
* @param {string} priority - WU priority
|
|
290
|
+
* @param {string} type - WU type
|
|
291
|
+
* @param {Object} opts - Additional options
|
|
292
|
+
* @returns {string} Relative path to created YAML file
|
|
293
|
+
*/
|
|
294
|
+
function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, opts = {}) {
|
|
295
|
+
const wuRelativePath = WU_PATHS.WU(id);
|
|
296
|
+
const wuAbsolutePath = join(worktreePath, wuRelativePath);
|
|
297
|
+
const wuDir = join(worktreePath, WU_PATHS.WU_DIR());
|
|
298
|
+
mkdirSync(wuDir, { recursive: true });
|
|
299
|
+
// WU-1428: Use todayISO() for consistent YYYY-MM-DD format (library-first)
|
|
300
|
+
const today = todayISO();
|
|
301
|
+
const wuContent = buildWUContent({
|
|
302
|
+
id,
|
|
303
|
+
lane,
|
|
304
|
+
title,
|
|
305
|
+
priority,
|
|
306
|
+
type,
|
|
307
|
+
created: today,
|
|
308
|
+
opts,
|
|
309
|
+
});
|
|
310
|
+
// WU-1539: Validate WU structure before writing (fail-fast, no placeholders)
|
|
264
311
|
// WU-1750: Zod transforms normalize embedded newlines in arrays and strings
|
|
265
|
-
const validationResult =
|
|
312
|
+
const validationResult = validateWU(wuContent);
|
|
266
313
|
if (!validationResult.success) {
|
|
267
314
|
const errors = validationResult.error.issues
|
|
268
315
|
.map((issue) => ` • ${issue.path.join('.')}: ${issue.message}`)
|
|
@@ -270,6 +317,14 @@ function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, o
|
|
|
270
317
|
die(`${LOG_PREFIX} ❌ WU YAML validation failed:\n\n${errors}\n\n` +
|
|
271
318
|
`Fix the issues above and retry.`);
|
|
272
319
|
}
|
|
320
|
+
const completenessResult = validateSpecCompleteness(wuContent, id);
|
|
321
|
+
if (!completenessResult.valid) {
|
|
322
|
+
const errorList = completenessResult.errors
|
|
323
|
+
.map((error) => ` • ${error}`)
|
|
324
|
+
.join(STRING_LITERALS.NEWLINE);
|
|
325
|
+
die(`${LOG_PREFIX} ❌ WU SPEC INCOMPLETE:\n\n${errorList}\n\n` +
|
|
326
|
+
`Provide the missing fields and retry.`);
|
|
327
|
+
}
|
|
273
328
|
// WU-2253: Validate acceptance/code_paths consistency and invariants compliance
|
|
274
329
|
// This blocks WU creation if acceptance references paths not in code_paths
|
|
275
330
|
// or if code_paths conflicts with tools/invariants.yml
|
|
@@ -429,76 +484,95 @@ async function main() {
|
|
|
429
484
|
if (!assignedTo) {
|
|
430
485
|
console.warn(`${LOG_PREFIX} ⚠️ No assigned_to set - WU will need manual assignment`);
|
|
431
486
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
487
|
+
const createSpecValidation = validateCreateSpec({
|
|
488
|
+
id: args.id,
|
|
489
|
+
lane: args.lane,
|
|
490
|
+
title: args.title,
|
|
491
|
+
priority: args.priority || DEFAULT_PRIORITY,
|
|
492
|
+
type: args.type || DEFAULT_TYPE,
|
|
493
|
+
opts: {
|
|
494
|
+
description: args.description,
|
|
495
|
+
acceptance: args.acceptance,
|
|
496
|
+
codePaths: args.codePaths,
|
|
497
|
+
testPathsManual: args.testPathsManual,
|
|
498
|
+
testPathsUnit: args.testPathsUnit,
|
|
499
|
+
testPathsE2e: args.testPathsE2e,
|
|
500
|
+
exposure: args.exposure,
|
|
501
|
+
userJourney: args.userJourney,
|
|
502
|
+
uiPairingWus: args.uiPairingWus,
|
|
503
|
+
specRefs: args.specRefs,
|
|
504
|
+
initiative: args.initiative,
|
|
505
|
+
phase: args.phase,
|
|
506
|
+
blockedBy: args.blockedBy,
|
|
507
|
+
blocks: args.blocks,
|
|
508
|
+
labels: args.labels,
|
|
509
|
+
assignedTo,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
if (!createSpecValidation.valid) {
|
|
513
|
+
const errorList = createSpecValidation.errors
|
|
514
|
+
.map((error) => ` • ${error}`)
|
|
515
|
+
.join(STRING_LITERALS.NEWLINE);
|
|
516
|
+
die(`${LOG_PREFIX} ❌ Spec validation failed:\n\n${errorList}`);
|
|
456
517
|
}
|
|
518
|
+
console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
|
|
457
519
|
// Transaction: micro-worktree isolation (WU-1439)
|
|
458
520
|
try {
|
|
459
521
|
const priority = args.priority || DEFAULT_PRIORITY;
|
|
460
522
|
const type = args.type || DEFAULT_TYPE;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
commitMessage,
|
|
498
|
-
files
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
523
|
+
const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
|
|
524
|
+
process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
|
|
525
|
+
try {
|
|
526
|
+
await withMicroWorktree({
|
|
527
|
+
operation: OPERATION_NAME,
|
|
528
|
+
id: args.id,
|
|
529
|
+
logPrefix: LOG_PREFIX,
|
|
530
|
+
execute: async ({ worktreePath }) => {
|
|
531
|
+
// Create WU YAML in micro-worktree
|
|
532
|
+
const wuPath = createWUYamlInWorktree(worktreePath, args.id, args.lane, args.title, priority, type, {
|
|
533
|
+
// Initiative system fields (WU-1247)
|
|
534
|
+
initiative: args.initiative,
|
|
535
|
+
phase: args.phase,
|
|
536
|
+
blockedBy: args.blockedBy,
|
|
537
|
+
blocks: args.blocks,
|
|
538
|
+
labels: args.labels,
|
|
539
|
+
// WU-1368: Assigned to
|
|
540
|
+
assignedTo,
|
|
541
|
+
// WU-1364: Full spec inline options
|
|
542
|
+
description: args.description,
|
|
543
|
+
acceptance: args.acceptance,
|
|
544
|
+
codePaths: args.codePaths,
|
|
545
|
+
testPathsManual: args.testPathsManual,
|
|
546
|
+
testPathsUnit: args.testPathsUnit,
|
|
547
|
+
testPathsE2e: args.testPathsE2e,
|
|
548
|
+
// WU-1998: Exposure field options
|
|
549
|
+
exposure: args.exposure,
|
|
550
|
+
userJourney: args.userJourney,
|
|
551
|
+
uiPairingWus: args.uiPairingWus,
|
|
552
|
+
// WU-2320: Spec references
|
|
553
|
+
specRefs: args.specRefs,
|
|
554
|
+
});
|
|
555
|
+
// Update backlog.md in micro-worktree
|
|
556
|
+
const backlogPath = updateBacklogInWorktree(worktreePath, args.id, args.lane, args.title);
|
|
557
|
+
// Build commit message
|
|
558
|
+
const shortTitle = truncateTitle(args.title);
|
|
559
|
+
const commitMessage = COMMIT_FORMATS.CREATE(args.id, shortTitle);
|
|
560
|
+
// Return commit message and files to commit
|
|
561
|
+
return {
|
|
562
|
+
commitMessage,
|
|
563
|
+
files: [wuPath, backlogPath],
|
|
564
|
+
};
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
if (previousWuTool === undefined) {
|
|
570
|
+
delete process.env.LUMENFLOW_WU_TOOL;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
process.env.LUMENFLOW_WU_TOOL = previousWuTool;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
502
576
|
console.log(`\n${LOG_PREFIX} ✅ Transaction complete!`);
|
|
503
577
|
console.log(`\nWU ${args.id} created successfully:`);
|
|
504
578
|
console.log(` File: ${WU_PATHS.WU(args.id)}`);
|
|
@@ -515,6 +589,7 @@ async function main() {
|
|
|
515
589
|
}
|
|
516
590
|
// Guard main() for testability (WU-1366)
|
|
517
591
|
import { fileURLToPath } from 'node:url';
|
|
592
|
+
import { runCLI } from './cli-entry-point.js';
|
|
518
593
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
519
|
-
main
|
|
594
|
+
runCLI(main);
|
|
520
595
|
}
|