@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.
Files changed (129) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +116 -0
  3. package/dist/gates.d.ts +41 -0
  4. package/dist/gates.d.ts.map +1 -0
  5. package/dist/gates.js +684 -0
  6. package/dist/gates.js.map +1 -0
  7. package/dist/initiative-add-wu.d.ts +22 -0
  8. package/dist/initiative-add-wu.d.ts.map +1 -0
  9. package/dist/initiative-add-wu.js +234 -0
  10. package/dist/initiative-add-wu.js.map +1 -0
  11. package/dist/initiative-create.d.ts +28 -0
  12. package/dist/initiative-create.d.ts.map +1 -0
  13. package/dist/initiative-create.js +172 -0
  14. package/dist/initiative-create.js.map +1 -0
  15. package/dist/initiative-edit.d.ts +34 -0
  16. package/dist/initiative-edit.d.ts.map +1 -0
  17. package/dist/initiative-edit.js +440 -0
  18. package/dist/initiative-edit.js.map +1 -0
  19. package/dist/initiative-list.d.ts +12 -0
  20. package/dist/initiative-list.d.ts.map +1 -0
  21. package/dist/initiative-list.js +101 -0
  22. package/dist/initiative-list.js.map +1 -0
  23. package/dist/initiative-status.d.ts +11 -0
  24. package/dist/initiative-status.d.ts.map +1 -0
  25. package/dist/initiative-status.js +221 -0
  26. package/dist/initiative-status.js.map +1 -0
  27. package/dist/mem-checkpoint.d.ts +16 -0
  28. package/dist/mem-checkpoint.d.ts.map +1 -0
  29. package/dist/mem-checkpoint.js +237 -0
  30. package/dist/mem-checkpoint.js.map +1 -0
  31. package/dist/mem-cleanup.d.ts +29 -0
  32. package/dist/mem-cleanup.d.ts.map +1 -0
  33. package/dist/mem-cleanup.js +267 -0
  34. package/dist/mem-cleanup.js.map +1 -0
  35. package/dist/mem-create.d.ts +17 -0
  36. package/dist/mem-create.d.ts.map +1 -0
  37. package/dist/mem-create.js +265 -0
  38. package/dist/mem-create.js.map +1 -0
  39. package/dist/mem-inbox.d.ts +35 -0
  40. package/dist/mem-inbox.d.ts.map +1 -0
  41. package/dist/mem-inbox.js +373 -0
  42. package/dist/mem-inbox.js.map +1 -0
  43. package/dist/mem-init.d.ts +15 -0
  44. package/dist/mem-init.d.ts.map +1 -0
  45. package/dist/mem-init.js +146 -0
  46. package/dist/mem-init.js.map +1 -0
  47. package/dist/mem-ready.d.ts +16 -0
  48. package/dist/mem-ready.d.ts.map +1 -0
  49. package/dist/mem-ready.js +224 -0
  50. package/dist/mem-ready.js.map +1 -0
  51. package/dist/mem-signal.d.ts +16 -0
  52. package/dist/mem-signal.d.ts.map +1 -0
  53. package/dist/mem-signal.js +204 -0
  54. package/dist/mem-signal.js.map +1 -0
  55. package/dist/mem-start.d.ts +16 -0
  56. package/dist/mem-start.d.ts.map +1 -0
  57. package/dist/mem-start.js +158 -0
  58. package/dist/mem-start.js.map +1 -0
  59. package/dist/mem-summarize.d.ts +22 -0
  60. package/dist/mem-summarize.d.ts.map +1 -0
  61. package/dist/mem-summarize.js +213 -0
  62. package/dist/mem-summarize.js.map +1 -0
  63. package/dist/mem-triage.d.ts +22 -0
  64. package/dist/mem-triage.d.ts.map +1 -0
  65. package/dist/mem-triage.js +328 -0
  66. package/dist/mem-triage.js.map +1 -0
  67. package/dist/spawn-list.d.ts +16 -0
  68. package/dist/spawn-list.d.ts.map +1 -0
  69. package/dist/spawn-list.js +140 -0
  70. package/dist/spawn-list.js.map +1 -0
  71. package/dist/wu-block.d.ts +16 -0
  72. package/dist/wu-block.d.ts.map +1 -0
  73. package/dist/wu-block.js +241 -0
  74. package/dist/wu-block.js.map +1 -0
  75. package/dist/wu-claim.d.ts +32 -0
  76. package/dist/wu-claim.d.ts.map +1 -0
  77. package/dist/wu-claim.js +1106 -0
  78. package/dist/wu-claim.js.map +1 -0
  79. package/dist/wu-cleanup.d.ts +17 -0
  80. package/dist/wu-cleanup.d.ts.map +1 -0
  81. package/dist/wu-cleanup.js +194 -0
  82. package/dist/wu-cleanup.js.map +1 -0
  83. package/dist/wu-create.d.ts +38 -0
  84. package/dist/wu-create.d.ts.map +1 -0
  85. package/dist/wu-create.js +520 -0
  86. package/dist/wu-create.js.map +1 -0
  87. package/dist/wu-deps.d.ts +13 -0
  88. package/dist/wu-deps.d.ts.map +1 -0
  89. package/dist/wu-deps.js +119 -0
  90. package/dist/wu-deps.js.map +1 -0
  91. package/dist/wu-done.d.ts +153 -0
  92. package/dist/wu-done.d.ts.map +1 -0
  93. package/dist/wu-done.js +2096 -0
  94. package/dist/wu-done.js.map +1 -0
  95. package/dist/wu-edit.d.ts +29 -0
  96. package/dist/wu-edit.d.ts.map +1 -0
  97. package/dist/wu-edit.js +852 -0
  98. package/dist/wu-edit.js.map +1 -0
  99. package/dist/wu-infer-lane.d.ts +17 -0
  100. package/dist/wu-infer-lane.d.ts.map +1 -0
  101. package/dist/wu-infer-lane.js +135 -0
  102. package/dist/wu-infer-lane.js.map +1 -0
  103. package/dist/wu-preflight.d.ts +47 -0
  104. package/dist/wu-preflight.d.ts.map +1 -0
  105. package/dist/wu-preflight.js +167 -0
  106. package/dist/wu-preflight.js.map +1 -0
  107. package/dist/wu-prune.d.ts +16 -0
  108. package/dist/wu-prune.d.ts.map +1 -0
  109. package/dist/wu-prune.js +259 -0
  110. package/dist/wu-prune.js.map +1 -0
  111. package/dist/wu-repair.d.ts +60 -0
  112. package/dist/wu-repair.d.ts.map +1 -0
  113. package/dist/wu-repair.js +226 -0
  114. package/dist/wu-repair.js.map +1 -0
  115. package/dist/wu-spawn-completion.d.ts +10 -0
  116. package/dist/wu-spawn-completion.js +30 -0
  117. package/dist/wu-spawn.d.ts +168 -0
  118. package/dist/wu-spawn.d.ts.map +1 -0
  119. package/dist/wu-spawn.js +1327 -0
  120. package/dist/wu-spawn.js.map +1 -0
  121. package/dist/wu-unblock.d.ts +16 -0
  122. package/dist/wu-unblock.d.ts.map +1 -0
  123. package/dist/wu-unblock.js +234 -0
  124. package/dist/wu-unblock.js.map +1 -0
  125. package/dist/wu-validate.d.ts +16 -0
  126. package/dist/wu-validate.d.ts.map +1 -0
  127. package/dist/wu-validate.js +193 -0
  128. package/dist/wu-validate.js.map +1 -0
  129. package/package.json +92 -0
@@ -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
+ });