@lumenflow/cli 1.0.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/LICENSE +190 -0
- package/README.md +116 -0
- package/dist/gates.d.ts +41 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +684 -0
- package/dist/gates.js.map +1 -0
- package/dist/initiative-add-wu.d.ts +22 -0
- package/dist/initiative-add-wu.d.ts.map +1 -0
- package/dist/initiative-add-wu.js +234 -0
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-create.d.ts +28 -0
- package/dist/initiative-create.d.ts.map +1 -0
- package/dist/initiative-create.js +172 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.d.ts +34 -0
- package/dist/initiative-edit.d.ts.map +1 -0
- package/dist/initiative-edit.js +440 -0
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.d.ts +12 -0
- package/dist/initiative-list.d.ts.map +1 -0
- package/dist/initiative-list.js +101 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-status.d.ts +11 -0
- package/dist/initiative-status.d.ts.map +1 -0
- package/dist/initiative-status.js +221 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/mem-checkpoint.d.ts +16 -0
- package/dist/mem-checkpoint.d.ts.map +1 -0
- package/dist/mem-checkpoint.js +237 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.d.ts +29 -0
- package/dist/mem-cleanup.d.ts.map +1 -0
- package/dist/mem-cleanup.js +267 -0
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-create.d.ts +17 -0
- package/dist/mem-create.d.ts.map +1 -0
- package/dist/mem-create.js +265 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-inbox.d.ts +35 -0
- package/dist/mem-inbox.d.ts.map +1 -0
- package/dist/mem-inbox.js +373 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-init.d.ts +15 -0
- package/dist/mem-init.d.ts.map +1 -0
- package/dist/mem-init.js +146 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-ready.d.ts +16 -0
- package/dist/mem-ready.d.ts.map +1 -0
- package/dist/mem-ready.js +224 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-signal.d.ts +16 -0
- package/dist/mem-signal.d.ts.map +1 -0
- package/dist/mem-signal.js +204 -0
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.d.ts +16 -0
- package/dist/mem-start.d.ts.map +1 -0
- package/dist/mem-start.js +158 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.d.ts +22 -0
- package/dist/mem-summarize.d.ts.map +1 -0
- package/dist/mem-summarize.js +213 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.d.ts +22 -0
- package/dist/mem-triage.d.ts.map +1 -0
- package/dist/mem-triage.js +328 -0
- package/dist/mem-triage.js.map +1 -0
- package/dist/spawn-list.d.ts +16 -0
- package/dist/spawn-list.d.ts.map +1 -0
- package/dist/spawn-list.js +140 -0
- package/dist/spawn-list.js.map +1 -0
- package/dist/wu-block.d.ts +16 -0
- package/dist/wu-block.d.ts.map +1 -0
- package/dist/wu-block.js +241 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-claim.d.ts +32 -0
- package/dist/wu-claim.d.ts.map +1 -0
- package/dist/wu-claim.js +1106 -0
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup.d.ts +17 -0
- package/dist/wu-cleanup.d.ts.map +1 -0
- package/dist/wu-cleanup.js +194 -0
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-create.d.ts +38 -0
- package/dist/wu-create.d.ts.map +1 -0
- package/dist/wu-create.js +520 -0
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-deps.d.ts +13 -0
- package/dist/wu-deps.d.ts.map +1 -0
- package/dist/wu-deps.js +119 -0
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done.d.ts +153 -0
- package/dist/wu-done.d.ts.map +1 -0
- package/dist/wu-done.js +2096 -0
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.d.ts +29 -0
- package/dist/wu-edit.d.ts.map +1 -0
- package/dist/wu-edit.js +852 -0
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.d.ts +17 -0
- package/dist/wu-infer-lane.d.ts.map +1 -0
- package/dist/wu-infer-lane.js +135 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.d.ts +47 -0
- package/dist/wu-preflight.d.ts.map +1 -0
- package/dist/wu-preflight.js +167 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prune.d.ts +16 -0
- package/dist/wu-prune.d.ts.map +1 -0
- package/dist/wu-prune.js +259 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-repair.d.ts +60 -0
- package/dist/wu-repair.d.ts.map +1 -0
- package/dist/wu-repair.js +226 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-spawn-completion.d.ts +10 -0
- package/dist/wu-spawn-completion.js +30 -0
- package/dist/wu-spawn.d.ts +168 -0
- package/dist/wu-spawn.d.ts.map +1 -0
- package/dist/wu-spawn.js +1327 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-unblock.d.ts +16 -0
- package/dist/wu-unblock.d.ts.map +1 -0
- package/dist/wu-unblock.js +234 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-validate.d.ts +16 -0
- package/dist/wu-validate.d.ts.map +1 -0
- package/dist/wu-validate.js +193 -0
- package/dist/wu-validate.js.map +1 -0
- package/package.json +92 -0
package/dist/wu-edit.js
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WU Edit Helper
|
|
4
|
+
*
|
|
5
|
+
* Race-safe WU spec editing using micro-worktree isolation (WU-1274).
|
|
6
|
+
*
|
|
7
|
+
* Enables editing WU YAML files without claiming the WU, perfect for:
|
|
8
|
+
* - Filling in placeholder content after wu:create
|
|
9
|
+
* - Updating description/acceptance criteria
|
|
10
|
+
* - Adding code_paths, notes, or other spec fields
|
|
11
|
+
*
|
|
12
|
+
* Uses the same micro-worktree pattern as wu:create (WU-1262):
|
|
13
|
+
* 1) Validate inputs (WU exists, status is ready)
|
|
14
|
+
* 2) Ensure main is clean and up-to-date with origin
|
|
15
|
+
* 3) Create temp branch WITHOUT switching (main checkout stays on main)
|
|
16
|
+
* 4) Create micro-worktree in /tmp pointing to temp branch
|
|
17
|
+
* 5) Apply edits in micro-worktree
|
|
18
|
+
* 6) Commit, ff-only merge, push
|
|
19
|
+
* 7) Cleanup temp branch and micro-worktree
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* pnpm wu:edit --id WU-123 --spec-file /path/to/spec.yaml
|
|
23
|
+
* pnpm wu:edit --id WU-123 --description "New description text"
|
|
24
|
+
* pnpm wu:edit --id WU-123 --acceptance "Criterion 1" --acceptance "Criterion 2"
|
|
25
|
+
*
|
|
26
|
+
* Part of WU-1274: Add wu:edit command for spec-only changes
|
|
27
|
+
* @see {@link tools/lib/micro-worktree.mjs} - Shared micro-worktree logic
|
|
28
|
+
*/
|
|
29
|
+
import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
|
|
30
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
31
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { join, resolve } from 'node:path';
|
|
33
|
+
// WU-1352: Use centralized YAML helper instead of raw js-yaml (Emergency fix Session 2)
|
|
34
|
+
// WU-1620: Import readWU for readiness summary
|
|
35
|
+
import { parseYAML, stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
36
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
37
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
38
|
+
import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, CLAIMED_MODES, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
|
|
39
|
+
// WU-1593: Use centralized validateWUIDFormat (DRY)
|
|
40
|
+
import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat, } from '@lumenflow/core/dist/wu-helpers.js';
|
|
41
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
42
|
+
import { validateLaneFormat } from '@lumenflow/core/dist/lane-checker.js';
|
|
43
|
+
// WU-1620: Import validateSpecCompleteness for readiness summary
|
|
44
|
+
// WU-1806: Import detectCurrentWorktree for worktree path resolution
|
|
45
|
+
import { defaultWorktreeFrom, validateSpecCompleteness, detectCurrentWorktree, } from '@lumenflow/core/dist/wu-done-validators.js';
|
|
46
|
+
import { validateReadyWU } from '@lumenflow/core/dist/wu-schema.js';
|
|
47
|
+
import { execSync } from 'node:child_process';
|
|
48
|
+
// WU-1442: Import date normalization to fix date corruption from js-yaml
|
|
49
|
+
import { normalizeToDateString } from '@lumenflow/core/dist/date-utils.js';
|
|
50
|
+
// WU-1929: Import initiative-related modules for bidirectional initiative updates
|
|
51
|
+
import { INIT_PATTERNS } from '@lumenflow/initiatives/dist/initiative-constants.js';
|
|
52
|
+
import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
|
|
53
|
+
import { readInitiative, writeInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
|
|
54
|
+
// WU-2004: Import schema normalization for legacy WU formats
|
|
55
|
+
import { normalizeWUSchema } from '@lumenflow/core/dist/wu-schema-normalization.js';
|
|
56
|
+
// WU-2253: Import WU spec linter for acceptance/code_paths validation
|
|
57
|
+
import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
|
|
58
|
+
/* eslint-disable security/detect-object-injection */
|
|
59
|
+
const PREFIX = LOG_PREFIX.EDIT;
|
|
60
|
+
/**
|
|
61
|
+
* Custom options for wu-edit (not in shared WU_OPTIONS)
|
|
62
|
+
*/
|
|
63
|
+
const EDIT_OPTIONS = {
|
|
64
|
+
specFile: {
|
|
65
|
+
name: 'specFile',
|
|
66
|
+
flags: '--spec-file <path>',
|
|
67
|
+
description: 'Path to YAML file with updated spec content',
|
|
68
|
+
},
|
|
69
|
+
description: {
|
|
70
|
+
name: 'description',
|
|
71
|
+
flags: '--description <text>',
|
|
72
|
+
description: 'New description text (replaces existing)',
|
|
73
|
+
},
|
|
74
|
+
acceptance: {
|
|
75
|
+
name: 'acceptance',
|
|
76
|
+
flags: '--acceptance <criterion>',
|
|
77
|
+
description: 'Acceptance criterion (repeatable, replaces existing; use --append to add)',
|
|
78
|
+
isRepeatable: true,
|
|
79
|
+
},
|
|
80
|
+
notes: {
|
|
81
|
+
name: 'notes',
|
|
82
|
+
flags: '--notes <text>',
|
|
83
|
+
description: 'New notes text (replaces existing)',
|
|
84
|
+
},
|
|
85
|
+
codePaths: {
|
|
86
|
+
name: 'codePaths',
|
|
87
|
+
flags: '--code-paths <path>',
|
|
88
|
+
description: 'Code path (repeatable, replaces existing; use --append to add)',
|
|
89
|
+
isRepeatable: true,
|
|
90
|
+
},
|
|
91
|
+
append: {
|
|
92
|
+
name: 'append',
|
|
93
|
+
flags: '--append',
|
|
94
|
+
description: 'Append to existing array values instead of replacing (for --acceptance, --code-paths)',
|
|
95
|
+
},
|
|
96
|
+
// WU-1456: Add lane reassignment support
|
|
97
|
+
lane: {
|
|
98
|
+
name: 'lane',
|
|
99
|
+
flags: '--lane <lane>',
|
|
100
|
+
description: 'New lane assignment (e.g., "Operations: Tooling")',
|
|
101
|
+
},
|
|
102
|
+
// WU-1620: Add type and priority edit support
|
|
103
|
+
type: {
|
|
104
|
+
name: 'type',
|
|
105
|
+
flags: '--type <type>',
|
|
106
|
+
description: 'New WU type (feature, bug, refactor, documentation)',
|
|
107
|
+
},
|
|
108
|
+
priority: {
|
|
109
|
+
name: 'priority',
|
|
110
|
+
flags: '--priority <priority>',
|
|
111
|
+
description: 'New priority (P0, P1, P2, P3)',
|
|
112
|
+
},
|
|
113
|
+
// WU-1929: Add initiative and phase edit support
|
|
114
|
+
initiative: {
|
|
115
|
+
name: 'initiative',
|
|
116
|
+
flags: '--initiative <initId>',
|
|
117
|
+
description: 'Initiative ID (e.g., INIT-001). Updates WU and initiative wus: arrays bidirectionally.',
|
|
118
|
+
},
|
|
119
|
+
phase: {
|
|
120
|
+
name: 'phase',
|
|
121
|
+
flags: '--phase <number>',
|
|
122
|
+
description: 'Phase number within initiative (e.g., 1, 2)',
|
|
123
|
+
},
|
|
124
|
+
// WU-2564: Add blocked_by and dependencies edit support
|
|
125
|
+
blockedBy: {
|
|
126
|
+
name: 'blockedBy',
|
|
127
|
+
flags: '--blocked-by <wuIds>',
|
|
128
|
+
description: 'Comma-separated WU IDs that block this WU (replaces existing; use --append to add)',
|
|
129
|
+
},
|
|
130
|
+
addDep: {
|
|
131
|
+
name: 'addDep',
|
|
132
|
+
flags: '--add-dep <wuIds>',
|
|
133
|
+
description: 'Comma-separated WU IDs to add to dependencies array (replaces existing; use --append to add)',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* WU-1929: Update initiative wus: arrays bidirectionally
|
|
138
|
+
*
|
|
139
|
+
* When a WU's initiative field changes, this function:
|
|
140
|
+
* 1. Removes the WU ID from the old initiative's wus: array (if exists)
|
|
141
|
+
* 2. Adds the WU ID to the new initiative's wus: array
|
|
142
|
+
*
|
|
143
|
+
* @param {string} worktreePath - Path to the worktree (for file operations)
|
|
144
|
+
* @param {string} wuId - WU ID being updated
|
|
145
|
+
* @param {string|undefined} oldInitId - Previous initiative ID (may be undefined)
|
|
146
|
+
* @param {string} newInitId - New initiative ID
|
|
147
|
+
* @returns {Array<string>} Array of relative file paths that were modified
|
|
148
|
+
*/
|
|
149
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
150
|
+
function updateInitiativeWusArrays(worktreePath, wuId, oldInitId, newInitId) {
|
|
151
|
+
const modifiedFiles = [];
|
|
152
|
+
// Remove from old initiative if it exists and is different from new
|
|
153
|
+
if (oldInitId && oldInitId !== newInitId) {
|
|
154
|
+
const oldInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(oldInitId));
|
|
155
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
156
|
+
if (existsSync(oldInitPath)) {
|
|
157
|
+
try {
|
|
158
|
+
const oldInit = readInitiative(oldInitPath, oldInitId);
|
|
159
|
+
if (Array.isArray(oldInit.wus) && oldInit.wus.includes(wuId)) {
|
|
160
|
+
oldInit.wus = oldInit.wus.filter((id) => id !== wuId);
|
|
161
|
+
writeInitiative(oldInitPath, oldInit);
|
|
162
|
+
modifiedFiles.push(INIT_PATHS.INITIATIVE(oldInitId));
|
|
163
|
+
console.log(`${PREFIX} ✅ Removed ${wuId} from ${oldInitId} wus: array`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// Old initiative may not exist or be invalid - log warning but continue
|
|
168
|
+
console.warn(`${PREFIX} ⚠️ Could not update old initiative ${oldInitId}: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Add to new initiative
|
|
173
|
+
const newInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(newInitId));
|
|
174
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
175
|
+
if (existsSync(newInitPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const newInit = readInitiative(newInitPath, newInitId);
|
|
178
|
+
if (!Array.isArray(newInit.wus)) {
|
|
179
|
+
newInit.wus = [];
|
|
180
|
+
}
|
|
181
|
+
if (!newInit.wus.includes(wuId)) {
|
|
182
|
+
newInit.wus.push(wuId);
|
|
183
|
+
writeInitiative(newInitPath, newInit);
|
|
184
|
+
modifiedFiles.push(INIT_PATHS.INITIATIVE(newInitId));
|
|
185
|
+
console.log(`${PREFIX} ✅ Added ${wuId} to ${newInitId} wus: array`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
die(`Failed to update new initiative ${newInitId}: ${err.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return modifiedFiles;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* WU-1929: Validate initiative ID format
|
|
196
|
+
* @param {string} initId - Initiative ID to validate
|
|
197
|
+
*/
|
|
198
|
+
function validateInitiativeFormat(initId) {
|
|
199
|
+
if (!INIT_PATTERNS.INIT_ID.test(initId)) {
|
|
200
|
+
die(`Invalid Initiative ID format: "${initId}"\n\n` +
|
|
201
|
+
`Expected format: INIT-<number> or INIT-<NAME> (e.g., INIT-001, INIT-TOOLING)`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* WU-1929: Validate initiative exists on disk
|
|
206
|
+
* @param {string} initId - Initiative ID to check
|
|
207
|
+
* @returns {string} Path to initiative file
|
|
208
|
+
*/
|
|
209
|
+
function validateInitiativeExists(initId) {
|
|
210
|
+
const initPath = INIT_PATHS.INITIATIVE(initId);
|
|
211
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
212
|
+
if (!existsSync(initPath)) {
|
|
213
|
+
die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
|
|
214
|
+
}
|
|
215
|
+
return initPath;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Parse command line arguments
|
|
219
|
+
*/
|
|
220
|
+
function parseArgs() {
|
|
221
|
+
return createWUParser({
|
|
222
|
+
name: 'wu-edit',
|
|
223
|
+
description: 'Edit WU spec files with micro-worktree isolation',
|
|
224
|
+
options: [
|
|
225
|
+
WU_OPTIONS.id,
|
|
226
|
+
EDIT_OPTIONS.specFile,
|
|
227
|
+
EDIT_OPTIONS.description,
|
|
228
|
+
EDIT_OPTIONS.acceptance,
|
|
229
|
+
EDIT_OPTIONS.notes,
|
|
230
|
+
EDIT_OPTIONS.codePaths,
|
|
231
|
+
EDIT_OPTIONS.append,
|
|
232
|
+
// WU-1390: Add test path flags
|
|
233
|
+
WU_OPTIONS.testPathsManual,
|
|
234
|
+
WU_OPTIONS.testPathsUnit,
|
|
235
|
+
WU_OPTIONS.testPathsE2e,
|
|
236
|
+
// WU-1456: Add lane reassignment
|
|
237
|
+
EDIT_OPTIONS.lane,
|
|
238
|
+
// WU-1620: Add type and priority
|
|
239
|
+
EDIT_OPTIONS.type,
|
|
240
|
+
EDIT_OPTIONS.priority,
|
|
241
|
+
// WU-1929: Add initiative and phase
|
|
242
|
+
EDIT_OPTIONS.initiative,
|
|
243
|
+
EDIT_OPTIONS.phase,
|
|
244
|
+
// WU-2564: Add blocked_by and dependencies
|
|
245
|
+
EDIT_OPTIONS.blockedBy,
|
|
246
|
+
EDIT_OPTIONS.addDep,
|
|
247
|
+
],
|
|
248
|
+
required: ['id'],
|
|
249
|
+
allowPositionalId: true,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* WU-1620: Display readiness summary after edit
|
|
254
|
+
*
|
|
255
|
+
* Shows whether WU is ready for wu:claim based on spec completeness.
|
|
256
|
+
* Non-blocking - just informational to help agents understand what's missing.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} id - WU ID
|
|
259
|
+
*/
|
|
260
|
+
function displayReadinessSummary(id) {
|
|
261
|
+
try {
|
|
262
|
+
const wuPath = WU_PATHS.WU(id);
|
|
263
|
+
const wuDoc = readWU(wuPath, id);
|
|
264
|
+
const { valid, errors } = validateSpecCompleteness(wuDoc, id);
|
|
265
|
+
const { BOX, BOX_WIDTH, MESSAGES, ERROR_MAX_LENGTH, ERROR_TRUNCATE_LENGTH, TRUNCATION_SUFFIX, PADDING, } = READINESS_UI;
|
|
266
|
+
console.log(`\n${BOX.TOP_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.TOP_RIGHT}`);
|
|
267
|
+
if (valid) {
|
|
268
|
+
console.log(`${BOX.VERTICAL} ${MESSAGES.READY_YES}${''.padEnd(PADDING.READY_YES)}${BOX.VERTICAL}`);
|
|
269
|
+
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
270
|
+
const claimCmd = `Run: pnpm wu:claim --id ${id}`;
|
|
271
|
+
console.log(`${BOX.VERTICAL} ${claimCmd}${''.padEnd(BOX_WIDTH - claimCmd.length - 1)}${BOX.VERTICAL}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log(`${BOX.VERTICAL} ${MESSAGES.READY_NO}${''.padEnd(PADDING.READY_NO)}${BOX.VERTICAL}`);
|
|
275
|
+
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
276
|
+
console.log(`${BOX.VERTICAL} ${MESSAGES.MISSING_HEADER}${''.padEnd(PADDING.MISSING_HEADER)}${BOX.VERTICAL}`);
|
|
277
|
+
for (const error of errors) {
|
|
278
|
+
// Truncate long error messages to fit box
|
|
279
|
+
const truncated = error.length > ERROR_MAX_LENGTH
|
|
280
|
+
? `${error.substring(0, ERROR_TRUNCATE_LENGTH)}${TRUNCATION_SUFFIX}`
|
|
281
|
+
: error;
|
|
282
|
+
console.log(`${BOX.VERTICAL} ${MESSAGES.BULLET} ${truncated}${''.padEnd(Math.max(0, PADDING.ERROR_BULLET - truncated.length))}${BOX.VERTICAL}`);
|
|
283
|
+
}
|
|
284
|
+
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
285
|
+
const editCmd = `Run: pnpm wu:edit --id ${id} --help`;
|
|
286
|
+
console.log(`${BOX.VERTICAL} ${editCmd}${''.padEnd(BOX_WIDTH - editCmd.length - 1)}${BOX.VERTICAL}`);
|
|
287
|
+
}
|
|
288
|
+
console.log(`${BOX.BOTTOM_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.BOTTOM_RIGHT}`);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
// Non-blocking - if validation fails, just warn
|
|
292
|
+
console.warn(`${PREFIX} ⚠️ Could not validate readiness: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Edit modes for WU editing
|
|
297
|
+
* WU-1365: Worktree-aware editing support
|
|
298
|
+
*/
|
|
299
|
+
const EDIT_MODE = {
|
|
300
|
+
/** Ready WUs: Use micro-worktree on main (existing behavior) */
|
|
301
|
+
MICRO_WORKTREE: 'micro_worktree',
|
|
302
|
+
/** In-progress worktree WUs: Apply edits directly in active worktree (WU-1365) */
|
|
303
|
+
WORKTREE: 'worktree',
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Normalize date fields in WU object to prevent date corruption
|
|
307
|
+
*
|
|
308
|
+
* WU-1442: js-yaml parses unquoted YYYY-MM-DD dates as Date objects.
|
|
309
|
+
* When yaml.dump() serializes them back, it outputs ISO timestamps.
|
|
310
|
+
* This function normalizes Date objects back to YYYY-MM-DD strings.
|
|
311
|
+
*
|
|
312
|
+
* @param {object} wu - WU object from yaml.load()
|
|
313
|
+
* @returns {object} WU object with normalized date fields
|
|
314
|
+
*/
|
|
315
|
+
function normalizeWUDates(wu) {
|
|
316
|
+
if (wu.created !== undefined) {
|
|
317
|
+
wu.created = normalizeToDateString(wu.created);
|
|
318
|
+
}
|
|
319
|
+
return wu;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check WU exists and determine edit mode
|
|
323
|
+
* WU-1365: Now supports worktree-aware editing for in_progress WUs
|
|
324
|
+
*
|
|
325
|
+
* @param {string} id - WU ID
|
|
326
|
+
* @returns {{ wu: object, editMode: string }} WU object and edit mode
|
|
327
|
+
*/
|
|
328
|
+
function validateWUEditable(id) {
|
|
329
|
+
const wuPath = WU_PATHS.WU(id);
|
|
330
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
331
|
+
if (!existsSync(wuPath)) {
|
|
332
|
+
die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
|
|
333
|
+
}
|
|
334
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
|
|
335
|
+
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.ENCODING });
|
|
336
|
+
const wu = parseYAML(content);
|
|
337
|
+
// WU-1929: Done WUs allow initiative/phase edits only (metadata reassignment)
|
|
338
|
+
// WU-1365: Other fields on done WUs are immutable
|
|
339
|
+
if (wu.status === WU_STATUS.DONE) {
|
|
340
|
+
// Return done status - main() will validate allowed fields
|
|
341
|
+
return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: true };
|
|
342
|
+
}
|
|
343
|
+
// Handle in_progress WUs based on claimed_mode (WU-1365)
|
|
344
|
+
if (wu.status === WU_STATUS.IN_PROGRESS) {
|
|
345
|
+
const claimedMode = wu.claimed_mode || CLAIMED_MODES.WORKTREE; // Default to worktree for legacy WUs
|
|
346
|
+
// Block branch-only WUs with actionable guidance
|
|
347
|
+
if (claimedMode === CLAIMED_MODES.BRANCH_ONLY) {
|
|
348
|
+
die(`Cannot edit branch-only WU ${id} via wu:edit.\n\n` +
|
|
349
|
+
`WUs claimed with claimed_mode='${CLAIMED_MODES.BRANCH_ONLY}' cannot be edited via wu:edit.\n` +
|
|
350
|
+
`To modify the spec, edit the file directly on the lane branch and commit.`);
|
|
351
|
+
}
|
|
352
|
+
// Worktree mode WUs can be edited (WU-1365)
|
|
353
|
+
return { wu, editMode: EDIT_MODE.WORKTREE, isDone: false };
|
|
354
|
+
}
|
|
355
|
+
// Ready WUs use micro-worktree (existing behavior)
|
|
356
|
+
if (wu.status === WU_STATUS.READY) {
|
|
357
|
+
return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: false };
|
|
358
|
+
}
|
|
359
|
+
// Block other statuses (blocked, etc.)
|
|
360
|
+
die(`Cannot edit WU ${id}: status is '${wu.status}'.\n\n` +
|
|
361
|
+
`Only WUs in '${WU_STATUS.READY}' or '${WU_STATUS.IN_PROGRESS}' (worktree mode) can be edited.`);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Validate worktree exists on disk
|
|
365
|
+
* WU-1365: Required check before worktree editing
|
|
366
|
+
*
|
|
367
|
+
* @param {string} worktreePath - Absolute path to worktree
|
|
368
|
+
* @param {string} id - WU ID (for error messages)
|
|
369
|
+
*/
|
|
370
|
+
function validateWorktreeExists(worktreePath, id) {
|
|
371
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates worktree paths
|
|
372
|
+
if (!existsSync(worktreePath)) {
|
|
373
|
+
die(`Cannot edit WU ${id}: worktree path missing from disk.\n\n` +
|
|
374
|
+
`Expected worktree at: ${worktreePath}\n\n` +
|
|
375
|
+
`The worktree may have been removed or the path is incorrect.\n` +
|
|
376
|
+
`If the worktree was accidentally deleted, you may need to re-claim the WU.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Validate worktree has no uncommitted changes
|
|
381
|
+
* WU-1365: Required check to prevent edit conflicts
|
|
382
|
+
*
|
|
383
|
+
* @param {string} worktreePath - Absolute path to worktree
|
|
384
|
+
* @param {string} id - WU ID (for error messages)
|
|
385
|
+
*/
|
|
386
|
+
async function validateWorktreeClean(worktreePath, id) {
|
|
387
|
+
try {
|
|
388
|
+
const gitAdapter = createGitForPath(worktreePath);
|
|
389
|
+
const status = (await gitAdapter.raw(['status', '--porcelain'])).trim();
|
|
390
|
+
if (status !== '') {
|
|
391
|
+
die(`Cannot edit WU ${id}: worktree has uncommitted changes.\n\n` +
|
|
392
|
+
`Uncommitted changes in ${worktreePath}:\n${status}\n\n` +
|
|
393
|
+
`Commit or discard your changes before editing the WU spec:\n` +
|
|
394
|
+
` cd ${worktreePath}\n` +
|
|
395
|
+
` git add . && git commit -m "wip: save progress"\n\n` +
|
|
396
|
+
`Then retry wu:edit.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
die(`Cannot edit WU ${id}: failed to check worktree status.\n\n` +
|
|
401
|
+
`Error: ${err.message}\n\n` +
|
|
402
|
+
`Worktree path: ${worktreePath}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Validate worktree is on expected lane branch
|
|
407
|
+
* WU-1365: Prevents editing WUs in worktrees with mismatched branches
|
|
408
|
+
*
|
|
409
|
+
* @param {string} worktreePath - Absolute path to worktree
|
|
410
|
+
* @param {string} expectedBranch - Expected branch name (e.g., lane/operations-tooling/wu-1365)
|
|
411
|
+
* @param {string} id - WU ID (for error messages)
|
|
412
|
+
*/
|
|
413
|
+
async function validateWorktreeBranch(worktreePath, expectedBranch, id) {
|
|
414
|
+
try {
|
|
415
|
+
const gitAdapter = createGitForPath(worktreePath);
|
|
416
|
+
const actualBranch = (await gitAdapter.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
417
|
+
if (actualBranch !== expectedBranch) {
|
|
418
|
+
die(`Cannot edit WU ${id}: worktree branch does not match expected lane branch.\n\n` +
|
|
419
|
+
`Expected branch: ${expectedBranch}\n` +
|
|
420
|
+
`Actual branch: ${actualBranch}\n\n` +
|
|
421
|
+
`This may indicate a corrupted worktree state.\n` +
|
|
422
|
+
`Verify the worktree is correctly set up for this WU.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
die(`Cannot edit WU ${id}: failed to check worktree branch.\n\n` +
|
|
427
|
+
`Error: ${err.message}\n\n` +
|
|
428
|
+
`Worktree path: ${worktreePath}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Apply edits directly in an active worktree (WU-1365)
|
|
433
|
+
* Used for in_progress WUs with claimed_mode=worktree
|
|
434
|
+
*
|
|
435
|
+
* @param {object} params - Parameters
|
|
436
|
+
* @param {string} params.worktreePath - Absolute path to worktree
|
|
437
|
+
* @param {string} params.id - WU ID
|
|
438
|
+
* @param {object} params.updatedWU - Updated WU object
|
|
439
|
+
*/
|
|
440
|
+
async function applyEditsInWorktree({ worktreePath, id, updatedWU }) {
|
|
441
|
+
const wuPath = join(worktreePath, WU_PATHS.WU(id));
|
|
442
|
+
// WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
|
|
443
|
+
normalizeWUDates(updatedWU);
|
|
444
|
+
// Emergency fix Session 2: Use centralized stringifyYAML helper
|
|
445
|
+
const yamlContent = stringifyYAML(updatedWU);
|
|
446
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
|
|
447
|
+
writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
448
|
+
console.log(`${PREFIX} ✅ Updated ${id}.yaml in worktree`);
|
|
449
|
+
// Format the file
|
|
450
|
+
try {
|
|
451
|
+
execSync(`${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} "${wuPath}"`, {
|
|
452
|
+
cwd: worktreePath,
|
|
453
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
454
|
+
stdio: 'pipe',
|
|
455
|
+
});
|
|
456
|
+
console.log(`${PREFIX} ✅ Formatted ${id}.yaml`);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.warn(`${PREFIX} ⚠️ Could not format file: ${err.message}`);
|
|
460
|
+
}
|
|
461
|
+
// Stage and commit using git adapter (library-first)
|
|
462
|
+
const commitMsg = COMMIT_FORMATS.SPEC_UPDATE(id);
|
|
463
|
+
try {
|
|
464
|
+
const gitAdapter = createGitForPath(worktreePath);
|
|
465
|
+
await gitAdapter.add(wuPath);
|
|
466
|
+
await gitAdapter.commit(commitMsg);
|
|
467
|
+
console.log(`${PREFIX} ✅ Committed: ${commitMsg}`);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
die(`Failed to commit edit in worktree.\n\n` +
|
|
471
|
+
`Error: ${err.message}\n\n` +
|
|
472
|
+
`The WU file was updated but could not be committed.\n` +
|
|
473
|
+
`You may need to commit manually in the worktree.`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Ensure working tree is clean
|
|
478
|
+
*/
|
|
479
|
+
async function ensureCleanWorkingTree() {
|
|
480
|
+
const status = await getGitForCwd().getStatus();
|
|
481
|
+
if (status.trim()) {
|
|
482
|
+
die(`Working tree is not clean. Cannot edit WU.\n\nUncommitted changes:\n${status}\n\nCommit or stash changes before editing:\n git add . && git commit -m "..."\n`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Merge array values: replace by default, append if --append flag is set (WU-1388)
|
|
487
|
+
* @param {Array} existing - Current array value from WU
|
|
488
|
+
* @param {Array} newValues - New values from CLI
|
|
489
|
+
* @param {boolean} shouldAppend - Whether to append instead of replace
|
|
490
|
+
* @returns {Array} Merged array
|
|
491
|
+
*/
|
|
492
|
+
function mergeArrayField(existing, newValues, shouldAppend) {
|
|
493
|
+
if (!shouldAppend) {
|
|
494
|
+
return newValues;
|
|
495
|
+
}
|
|
496
|
+
const existingArray = Array.isArray(existing) ? existing : [];
|
|
497
|
+
return [...existingArray, ...newValues];
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Load spec file and merge with original WU (preserving id and status)
|
|
501
|
+
* @param {string} specPath - Path to spec file
|
|
502
|
+
* @param {object} originalWU - Original WU object
|
|
503
|
+
* @returns {object} Merged WU object
|
|
504
|
+
*/
|
|
505
|
+
function loadSpecFile(specPath, originalWU) {
|
|
506
|
+
const resolvedPath = resolve(specPath);
|
|
507
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates spec files
|
|
508
|
+
if (!existsSync(resolvedPath)) {
|
|
509
|
+
die(`Spec file not found: ${resolvedPath}`);
|
|
510
|
+
}
|
|
511
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates spec files
|
|
512
|
+
const specContent = readFileSync(resolvedPath, {
|
|
513
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
514
|
+
});
|
|
515
|
+
const newSpec = parseYAML(specContent);
|
|
516
|
+
// Preserve id and status from original (cannot be changed via edit)
|
|
517
|
+
return {
|
|
518
|
+
...newSpec,
|
|
519
|
+
id: originalWU.id,
|
|
520
|
+
status: originalWU.status,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Apply edits to WU YAML
|
|
525
|
+
* Returns the updated WU object
|
|
526
|
+
*/
|
|
527
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
528
|
+
function applyEdits(wu, opts) {
|
|
529
|
+
// Full spec replacement from file
|
|
530
|
+
if (opts.specFile) {
|
|
531
|
+
return loadSpecFile(opts.specFile, wu);
|
|
532
|
+
}
|
|
533
|
+
const updated = { ...wu };
|
|
534
|
+
// Field-level updates
|
|
535
|
+
if (opts.description) {
|
|
536
|
+
updated.description = opts.description;
|
|
537
|
+
}
|
|
538
|
+
// Handle repeatable --acceptance flags (WU-1388: replace by default, append with --append)
|
|
539
|
+
if (opts.acceptance && opts.acceptance.length > 0) {
|
|
540
|
+
updated.acceptance = mergeArrayField(wu.acceptance, opts.acceptance, opts.append);
|
|
541
|
+
}
|
|
542
|
+
if (opts.notes) {
|
|
543
|
+
updated.notes = opts.notes;
|
|
544
|
+
}
|
|
545
|
+
// WU-1456: Handle lane reassignment
|
|
546
|
+
if (opts.lane) {
|
|
547
|
+
validateLaneFormat(opts.lane);
|
|
548
|
+
updated.lane = opts.lane;
|
|
549
|
+
}
|
|
550
|
+
// WU-1620: Handle type and priority updates
|
|
551
|
+
if (opts.type) {
|
|
552
|
+
updated.type = opts.type;
|
|
553
|
+
}
|
|
554
|
+
if (opts.priority) {
|
|
555
|
+
updated.priority = opts.priority;
|
|
556
|
+
}
|
|
557
|
+
// WU-1929: Handle initiative and phase updates
|
|
558
|
+
// Note: Initiative bidirectional updates (initiative wus: arrays) are handled separately
|
|
559
|
+
// in the main function after applyEdits, since they require file I/O
|
|
560
|
+
if (opts.initiative) {
|
|
561
|
+
validateInitiativeFormat(opts.initiative);
|
|
562
|
+
validateInitiativeExists(opts.initiative);
|
|
563
|
+
updated.initiative = opts.initiative;
|
|
564
|
+
}
|
|
565
|
+
if (opts.phase !== undefined && opts.phase !== null) {
|
|
566
|
+
const phaseNum = parseInt(opts.phase, 10);
|
|
567
|
+
if (isNaN(phaseNum) || phaseNum < 1) {
|
|
568
|
+
die(`Invalid phase number: "${opts.phase}"\n\nPhase must be a positive integer (e.g., 1, 2, 3)`);
|
|
569
|
+
}
|
|
570
|
+
updated.phase = phaseNum;
|
|
571
|
+
}
|
|
572
|
+
// Handle repeatable --code-paths flags (WU-1388: replace by default, append with --append)
|
|
573
|
+
// WU-1816: Split comma-separated string into array (same pattern as test paths)
|
|
574
|
+
// WU-1870: Fix to split comma-separated values WITHIN array elements (Commander passes ['a,b'] not 'a,b')
|
|
575
|
+
if (opts.codePaths && opts.codePaths.length > 0) {
|
|
576
|
+
const rawCodePaths = opts.codePaths;
|
|
577
|
+
const codePaths = Array.isArray(rawCodePaths)
|
|
578
|
+
? rawCodePaths
|
|
579
|
+
.flatMap((p) => p.split(','))
|
|
580
|
+
.map((p) => p.trim())
|
|
581
|
+
.filter(Boolean)
|
|
582
|
+
: rawCodePaths
|
|
583
|
+
.split(',')
|
|
584
|
+
.map((p) => p.trim())
|
|
585
|
+
.filter(Boolean);
|
|
586
|
+
updated.code_paths = mergeArrayField(wu.code_paths, codePaths, opts.append);
|
|
587
|
+
}
|
|
588
|
+
// WU-1390: Handle test path flags (DRY refactor)
|
|
589
|
+
const testPathMappings = [
|
|
590
|
+
{ optKey: 'testPathsManual', field: 'manual' },
|
|
591
|
+
{ optKey: 'testPathsUnit', field: 'unit' },
|
|
592
|
+
{ optKey: 'testPathsE2e', field: 'e2e' },
|
|
593
|
+
];
|
|
594
|
+
for (const { optKey, field } of testPathMappings) {
|
|
595
|
+
const rawPaths = opts[optKey];
|
|
596
|
+
if (rawPaths && rawPaths.length > 0) {
|
|
597
|
+
// Split comma-separated string into array (options are comma-separated per description)
|
|
598
|
+
// WU-1870: Fix to split comma-separated values WITHIN array elements
|
|
599
|
+
const paths = Array.isArray(rawPaths)
|
|
600
|
+
? rawPaths
|
|
601
|
+
.flatMap((p) => p.split(','))
|
|
602
|
+
.map((p) => p.trim())
|
|
603
|
+
.filter(Boolean)
|
|
604
|
+
: rawPaths
|
|
605
|
+
.split(',')
|
|
606
|
+
.map((p) => p.trim())
|
|
607
|
+
.filter(Boolean);
|
|
608
|
+
updated.tests = updated.tests || {};
|
|
609
|
+
updated.tests[field] = mergeArrayField(wu.tests?.[field], paths, opts.append);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// WU-2564: Handle --blocked-by flag
|
|
613
|
+
// Comma-separated WU IDs that block this WU
|
|
614
|
+
if (opts.blockedBy) {
|
|
615
|
+
const rawBlockedBy = opts.blockedBy;
|
|
616
|
+
const blockedByIds = rawBlockedBy
|
|
617
|
+
.split(',')
|
|
618
|
+
.map((id) => id.trim())
|
|
619
|
+
.filter(Boolean);
|
|
620
|
+
updated.blocked_by = mergeArrayField(wu.blocked_by, blockedByIds, opts.append);
|
|
621
|
+
}
|
|
622
|
+
// WU-2564: Handle --add-dep flag
|
|
623
|
+
// Comma-separated WU IDs to add to dependencies array
|
|
624
|
+
if (opts.addDep) {
|
|
625
|
+
const rawAddDep = opts.addDep;
|
|
626
|
+
const depIds = rawAddDep
|
|
627
|
+
.split(',')
|
|
628
|
+
.map((id) => id.trim())
|
|
629
|
+
.filter(Boolean);
|
|
630
|
+
updated.dependencies = mergeArrayField(wu.dependencies, depIds, opts.append);
|
|
631
|
+
}
|
|
632
|
+
return updated;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Main entry point
|
|
636
|
+
*/
|
|
637
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
638
|
+
async function main() {
|
|
639
|
+
const opts = parseArgs();
|
|
640
|
+
const { id } = opts;
|
|
641
|
+
console.log(`${PREFIX} Starting WU edit for ${id}`);
|
|
642
|
+
// Validate inputs
|
|
643
|
+
validateWUIDFormat(id);
|
|
644
|
+
const { wu: originalWU, editMode, isDone } = validateWUEditable(id);
|
|
645
|
+
// WU-1929: Done WUs only allow initiative/phase edits (metadata reassignment)
|
|
646
|
+
if (isDone) {
|
|
647
|
+
const disallowedEdits = [];
|
|
648
|
+
if (opts.specFile)
|
|
649
|
+
disallowedEdits.push('--spec-file');
|
|
650
|
+
if (opts.description)
|
|
651
|
+
disallowedEdits.push('--description');
|
|
652
|
+
if (opts.acceptance && opts.acceptance.length > 0)
|
|
653
|
+
disallowedEdits.push('--acceptance');
|
|
654
|
+
if (opts.notes)
|
|
655
|
+
disallowedEdits.push('--notes');
|
|
656
|
+
if (opts.codePaths && opts.codePaths.length > 0)
|
|
657
|
+
disallowedEdits.push('--code-paths');
|
|
658
|
+
if (opts.lane)
|
|
659
|
+
disallowedEdits.push('--lane');
|
|
660
|
+
if (opts.type)
|
|
661
|
+
disallowedEdits.push('--type');
|
|
662
|
+
if (opts.priority)
|
|
663
|
+
disallowedEdits.push('--priority');
|
|
664
|
+
if (opts.testPathsManual && opts.testPathsManual.length > 0)
|
|
665
|
+
disallowedEdits.push('--test-paths-manual');
|
|
666
|
+
if (opts.testPathsUnit && opts.testPathsUnit.length > 0)
|
|
667
|
+
disallowedEdits.push('--test-paths-unit');
|
|
668
|
+
if (opts.testPathsE2e && opts.testPathsE2e.length > 0)
|
|
669
|
+
disallowedEdits.push('--test-paths-e2e');
|
|
670
|
+
if (disallowedEdits.length > 0) {
|
|
671
|
+
die(`Cannot edit WU ${id}: WU is done/immutable.\n\n` +
|
|
672
|
+
`Completed WUs only allow initiative/phase reassignment.\n` +
|
|
673
|
+
`Disallowed edits: ${disallowedEdits.join(', ')}\n\n` +
|
|
674
|
+
`Allowed for done WUs:\n` +
|
|
675
|
+
` --initiative <initId> Reassign to different initiative\n` +
|
|
676
|
+
` --phase <number> Update phase within initiative`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Check we have something to edit
|
|
680
|
+
// Note: repeatable options (acceptance, codePaths, testPaths*) default to empty arrays,
|
|
681
|
+
// so we check .length instead of truthiness
|
|
682
|
+
const hasEdits = opts.specFile ||
|
|
683
|
+
opts.description ||
|
|
684
|
+
(opts.acceptance && opts.acceptance.length > 0) ||
|
|
685
|
+
opts.notes ||
|
|
686
|
+
(opts.codePaths && opts.codePaths.length > 0) ||
|
|
687
|
+
// WU-1390: Add test path flags to hasEdits check
|
|
688
|
+
(opts.testPathsManual && opts.testPathsManual.length > 0) ||
|
|
689
|
+
(opts.testPathsUnit && opts.testPathsUnit.length > 0) ||
|
|
690
|
+
(opts.testPathsE2e && opts.testPathsE2e.length > 0) ||
|
|
691
|
+
// WU-1456: Add lane to hasEdits check
|
|
692
|
+
opts.lane ||
|
|
693
|
+
// WU-1620: Add type and priority to hasEdits check
|
|
694
|
+
opts.type ||
|
|
695
|
+
opts.priority ||
|
|
696
|
+
// WU-1929: Add initiative and phase to hasEdits check
|
|
697
|
+
opts.initiative ||
|
|
698
|
+
opts.phase ||
|
|
699
|
+
// WU-2564: Add blocked_by and add_dep to hasEdits check
|
|
700
|
+
opts.blockedBy ||
|
|
701
|
+
opts.addDep;
|
|
702
|
+
if (!hasEdits) {
|
|
703
|
+
die('No edits specified.\n\n' +
|
|
704
|
+
'Provide one of:\n' +
|
|
705
|
+
' --spec-file <path> Replace full spec from YAML file\n' +
|
|
706
|
+
' --description <text> Update description field\n' +
|
|
707
|
+
' --acceptance <text> Replace acceptance criteria (repeatable; use --append to add)\n' +
|
|
708
|
+
' --notes <text> Update notes field\n' +
|
|
709
|
+
' --code-paths <paths> Replace code paths (repeatable; use --append to add)\n' +
|
|
710
|
+
' --lane <lane> Update lane assignment (e.g., "Operations: Tooling")\n' +
|
|
711
|
+
' --type <type> Update WU type (feature, bug, refactor, documentation)\n' +
|
|
712
|
+
' --priority <priority> Update priority (P0, P1, P2, P3)\n' +
|
|
713
|
+
' --initiative <initId> Update initiative (bidirectional update)\n' +
|
|
714
|
+
' --phase <number> Update phase within initiative\n' +
|
|
715
|
+
' --test-paths-manual <t> Add manual test descriptions (repeatable; use --append to add)\n' +
|
|
716
|
+
' --test-paths-unit <path> Add unit test paths (repeatable; use --append to add)\n' +
|
|
717
|
+
' --test-paths-e2e <path> Add e2e test paths (repeatable; use --append to add)\n' +
|
|
718
|
+
' --blocked-by <wuIds> WU IDs that block this WU (comma-separated; use --append to add)\n' +
|
|
719
|
+
' --add-dep <wuIds> Add WU IDs to dependencies (comma-separated; use --append to add)');
|
|
720
|
+
}
|
|
721
|
+
// Apply edits to get updated WU
|
|
722
|
+
const updatedWU = applyEdits(originalWU, opts);
|
|
723
|
+
// WU-2004: Normalize legacy schema fields before validation
|
|
724
|
+
// Converts: summary→description, string risks→array, test_paths→tests, etc.
|
|
725
|
+
const normalizedForValidation = normalizeWUSchema(updatedWU);
|
|
726
|
+
// WU-1539: Validate WU structure after applying edits (fail-fast, allows placeholders)
|
|
727
|
+
// WU-1750: Zod transforms normalize embedded newlines in arrays and strings
|
|
728
|
+
const validationResult = validateReadyWU(normalizedForValidation);
|
|
729
|
+
if (!validationResult.success) {
|
|
730
|
+
const errors = validationResult.error.issues
|
|
731
|
+
.map((issue) => ` • ${issue.path.join('.')}: ${issue.message}`)
|
|
732
|
+
.join('\n');
|
|
733
|
+
die(`${PREFIX} ❌ WU YAML validation failed:\n\n${errors}\n\nFix the issues above and retry.`);
|
|
734
|
+
}
|
|
735
|
+
// WU-2253: Validate acceptance/code_paths consistency and invariants compliance
|
|
736
|
+
// This blocks WU edits if acceptance references paths not in code_paths
|
|
737
|
+
// or if code_paths conflicts with tools/invariants.yml
|
|
738
|
+
const invariantsPath = join(process.cwd(), 'tools/invariants.yml');
|
|
739
|
+
const lintResult = lintWUSpec(normalizedForValidation, { invariantsPath });
|
|
740
|
+
if (!lintResult.valid) {
|
|
741
|
+
const formatted = formatLintErrors(lintResult.errors);
|
|
742
|
+
die(`${PREFIX} ❌ WU SPEC LINT FAILED:\n\n${formatted}\n` +
|
|
743
|
+
`Fix the issues above before editing this WU.`);
|
|
744
|
+
}
|
|
745
|
+
// WU-1750: CRITICAL - Use transformed data for all subsequent operations
|
|
746
|
+
// This ensures embedded newlines are normalized before YAML output
|
|
747
|
+
const normalizedWU = validationResult.data;
|
|
748
|
+
// Validate lane format if present (WU-923: block parent-only lanes with taxonomy)
|
|
749
|
+
if (normalizedWU.lane) {
|
|
750
|
+
validateLaneFormat(normalizedWU.lane);
|
|
751
|
+
}
|
|
752
|
+
// WU-1365: Handle based on edit mode
|
|
753
|
+
if (editMode === EDIT_MODE.WORKTREE) {
|
|
754
|
+
// WU-1929: Block initiative changes for in_progress WUs
|
|
755
|
+
// Initiative files are on main, not in worktrees, so bidirectional updates
|
|
756
|
+
// cannot be done atomically. Users should complete the WU first.
|
|
757
|
+
if (opts.initiative && opts.initiative !== originalWU.initiative) {
|
|
758
|
+
die(`Cannot change initiative for in_progress WU ${id}.\n\n` +
|
|
759
|
+
`Initiative reassignment requires atomic updates to initiative YAML files on main,\n` +
|
|
760
|
+
`which is not possible while the WU is in_progress.\n\n` +
|
|
761
|
+
`Options:\n` +
|
|
762
|
+
` 1. Complete the WU first: pnpm wu:done --id ${id}\n` +
|
|
763
|
+
` Then reassign: pnpm wu:edit --id ${id} --initiative ${opts.initiative}\n` +
|
|
764
|
+
` 2. Block the WU if not ready to complete:\n` +
|
|
765
|
+
` pnpm wu:block --id ${id} --reason "Needs initiative reassignment"`);
|
|
766
|
+
}
|
|
767
|
+
// In-progress worktree WUs: apply edits directly in the active worktree
|
|
768
|
+
console.log(`${PREFIX} Editing in_progress WU in active worktree...`);
|
|
769
|
+
// Resolve worktree path using defaultWorktreeFrom() helper
|
|
770
|
+
const worktreePath = await defaultWorktreeFrom(originalWU);
|
|
771
|
+
if (!worktreePath) {
|
|
772
|
+
die(`Cannot determine worktree path for WU ${id}.\n\n` +
|
|
773
|
+
`Check that worktree_path is set in the WU YAML or lane field is valid.`);
|
|
774
|
+
}
|
|
775
|
+
// WU-1806: Resolve to absolute path correctly even when running from inside a worktree
|
|
776
|
+
// If we're already inside a worktree, check if it matches the target worktree
|
|
777
|
+
const currentWorktree = detectCurrentWorktree();
|
|
778
|
+
let absoluteWorktreePath;
|
|
779
|
+
if (currentWorktree && currentWorktree.endsWith(worktreePath.replace('worktrees/', ''))) {
|
|
780
|
+
// We're inside the target worktree - use cwd directly
|
|
781
|
+
absoluteWorktreePath = currentWorktree;
|
|
782
|
+
console.log(`${PREFIX} Running from inside target worktree`);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
// Running from main checkout or a different worktree - resolve relative to cwd
|
|
786
|
+
// (which should be the main checkout in the typical case)
|
|
787
|
+
absoluteWorktreePath = resolve(worktreePath);
|
|
788
|
+
}
|
|
789
|
+
// Validate worktree state (WU-1365 acceptance criteria)
|
|
790
|
+
validateWorktreeExists(absoluteWorktreePath, id);
|
|
791
|
+
await validateWorktreeClean(absoluteWorktreePath, id);
|
|
792
|
+
// Calculate expected branch and validate
|
|
793
|
+
const expectedBranch = getLaneBranch(originalWU.lane, id);
|
|
794
|
+
await validateWorktreeBranch(absoluteWorktreePath, expectedBranch, id);
|
|
795
|
+
// Apply edits in the worktree (WU-1750: use normalized data)
|
|
796
|
+
await applyEditsInWorktree({
|
|
797
|
+
worktreePath: absoluteWorktreePath,
|
|
798
|
+
id,
|
|
799
|
+
updatedWU: normalizedWU,
|
|
800
|
+
});
|
|
801
|
+
console.log(`${PREFIX} ✅ Successfully edited ${id} in worktree`);
|
|
802
|
+
console.log(`${PREFIX} Changes committed to lane branch`);
|
|
803
|
+
// WU-1620: Display readiness summary
|
|
804
|
+
displayReadinessSummary(id);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
// Ready WUs: use micro-worktree on main (existing behavior)
|
|
808
|
+
// Pre-flight checks only needed for micro-worktree mode
|
|
809
|
+
await ensureOnMain(getGitForCwd());
|
|
810
|
+
await ensureCleanWorkingTree();
|
|
811
|
+
await ensureMainUpToDate(getGitForCwd(), 'wu:edit');
|
|
812
|
+
console.log(`${PREFIX} Applying edits via micro-worktree...`);
|
|
813
|
+
// WU-1929: Track old initiative for bidirectional update
|
|
814
|
+
const oldInitiative = originalWU.initiative;
|
|
815
|
+
const newInitiative = opts.initiative;
|
|
816
|
+
const initiativeChanged = newInitiative && newInitiative !== oldInitiative;
|
|
817
|
+
await withMicroWorktree({
|
|
818
|
+
operation: MICRO_WORKTREE_OPERATIONS.WU_EDIT,
|
|
819
|
+
id: id,
|
|
820
|
+
logPrefix: PREFIX,
|
|
821
|
+
execute: async ({ worktreePath }) => {
|
|
822
|
+
const files = [WU_PATHS.WU(id)];
|
|
823
|
+
// Write updated WU to micro-worktree (WU-1750: use normalized data)
|
|
824
|
+
const wuPath = join(worktreePath, WU_PATHS.WU(id));
|
|
825
|
+
// WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
|
|
826
|
+
normalizeWUDates(normalizedWU);
|
|
827
|
+
// Emergency fix Session 2: Use centralized stringifyYAML helper
|
|
828
|
+
const yamlContent = stringifyYAML(normalizedWU);
|
|
829
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
|
|
830
|
+
writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
831
|
+
console.log(`${PREFIX} ✅ Updated ${id}.yaml in micro-worktree`);
|
|
832
|
+
// WU-1929: Handle bidirectional initiative updates
|
|
833
|
+
if (initiativeChanged) {
|
|
834
|
+
const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
|
|
835
|
+
files.push(...initiativeFiles);
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
commitMessage: COMMIT_FORMATS.EDIT(id),
|
|
839
|
+
files,
|
|
840
|
+
};
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
console.log(`${PREFIX} ✅ Successfully edited ${id}`);
|
|
844
|
+
console.log(`${PREFIX} Changes pushed to origin/main`);
|
|
845
|
+
// WU-1620: Display readiness summary
|
|
846
|
+
displayReadinessSummary(id);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
main().catch((err) => {
|
|
850
|
+
console.error(`${PREFIX} ❌ ${err.message}`);
|
|
851
|
+
process.exit(EXIT_CODES.ERROR);
|
|
852
|
+
});
|