@lumenflow/cli 2.18.2 → 2.19.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/README.md +42 -41
- package/dist/delegation-list.js +140 -0
- package/dist/delegation-list.js.map +1 -0
- package/dist/doctor.js +35 -99
- package/dist/doctor.js.map +1 -1
- package/dist/gates-plan-resolvers.js +150 -0
- package/dist/gates-plan-resolvers.js.map +1 -0
- package/dist/gates-runners.js +533 -0
- package/dist/gates-runners.js.map +1 -0
- package/dist/gates-types.js +3 -0
- package/dist/gates-types.js.map +1 -1
- package/dist/gates-utils.js +316 -0
- package/dist/gates-utils.js.map +1 -0
- package/dist/gates.js +44 -1016
- package/dist/gates.js.map +1 -1
- package/dist/hooks/enforcement-generator.js +16 -880
- package/dist/hooks/enforcement-generator.js.map +1 -1
- package/dist/hooks/enforcement-sync.js +1 -4
- package/dist/hooks/enforcement-sync.js.map +1 -1
- package/dist/hooks/generators/auto-checkpoint.js +123 -0
- package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
- package/dist/hooks/generators/enforce-worktree.js +188 -0
- package/dist/hooks/generators/enforce-worktree.js.map +1 -0
- package/dist/hooks/generators/index.js +16 -0
- package/dist/hooks/generators/index.js.map +1 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
- package/dist/hooks/generators/require-wu.js +115 -0
- package/dist/hooks/generators/require-wu.js.map +1 -0
- package/dist/hooks/generators/session-start-recovery.js +101 -0
- package/dist/hooks/generators/session-start-recovery.js.map +1 -0
- package/dist/hooks/generators/signal-utils.js +52 -0
- package/dist/hooks/generators/signal-utils.js.map +1 -0
- package/dist/hooks/generators/warn-incomplete.js +65 -0
- package/dist/hooks/generators/warn-incomplete.js.map +1 -0
- package/dist/init-detection.js +228 -0
- package/dist/init-detection.js.map +1 -0
- package/dist/init-scaffolding.js +146 -0
- package/dist/init-scaffolding.js.map +1 -0
- package/dist/init-templates.js +1928 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.js +136 -2425
- package/dist/init.js.map +1 -1
- package/dist/initiative-edit.js +42 -11
- package/dist/initiative-edit.js.map +1 -1
- package/dist/initiative-remove-wu.js +0 -0
- package/dist/initiative-status.js +29 -2
- package/dist/initiative-status.js.map +1 -1
- package/dist/mem-context.js +22 -9
- package/dist/mem-context.js.map +1 -1
- package/dist/orchestrate-init-status.js +32 -1
- package/dist/orchestrate-init-status.js.map +1 -1
- package/dist/orchestrate-monitor.js +38 -38
- package/dist/orchestrate-monitor.js.map +1 -1
- package/dist/public-manifest.js +12 -5
- package/dist/public-manifest.js.map +1 -1
- package/dist/shared-validators.js +1 -0
- package/dist/shared-validators.js.map +1 -1
- package/dist/spawn-list.js +0 -0
- package/dist/wu-claim-branch.js +121 -0
- package/dist/wu-claim-branch.js.map +1 -0
- package/dist/wu-claim-output.js +83 -0
- package/dist/wu-claim-output.js.map +1 -0
- package/dist/wu-claim-resume-handler.js +85 -0
- package/dist/wu-claim-resume-handler.js.map +1 -0
- package/dist/wu-claim-state.js +572 -0
- package/dist/wu-claim-state.js.map +1 -0
- package/dist/wu-claim-validation.js +439 -0
- package/dist/wu-claim-validation.js.map +1 -0
- package/dist/wu-claim-worktree.js +221 -0
- package/dist/wu-claim-worktree.js.map +1 -0
- package/dist/wu-claim.js +54 -1402
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +254 -0
- package/dist/wu-create-content.js.map +1 -0
- package/dist/wu-create-readiness.js +57 -0
- package/dist/wu-create-readiness.js.map +1 -0
- package/dist/wu-create-validation.js +149 -0
- package/dist/wu-create-validation.js.map +1 -0
- package/dist/wu-create.js +39 -441
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +144 -249
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +432 -0
- package/dist/wu-edit-operations.js.map +1 -0
- package/dist/wu-edit-validators.js +280 -0
- package/dist/wu-edit-validators.js.map +1 -0
- package/dist/wu-edit.js +27 -713
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-prep.js +32 -2
- package/dist/wu-prep.js.map +1 -1
- package/dist/wu-repair.js +1 -1
- package/dist/wu-repair.js.map +1 -1
- package/dist/wu-spawn-prompt-builders.js +1123 -0
- package/dist/wu-spawn-prompt-builders.js.map +1 -0
- package/dist/wu-spawn-strategy-resolver.js +314 -0
- package/dist/wu-spawn-strategy-resolver.js.map +1 -0
- package/dist/wu-spawn.js +9 -1398
- package/dist/wu-spawn.js.map +1 -1
- package/package.json +10 -7
- package/templates/core/LUMENFLOW.md.template +29 -99
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/wu-edit.js
CHANGED
|
@@ -24,130 +24,39 @@
|
|
|
24
24
|
* pnpm wu:edit --id WU-123 --acceptance "Criterion 1" --acceptance "Criterion 2"
|
|
25
25
|
*
|
|
26
26
|
* Part of WU-1274: Add wu:edit command for spec-only changes
|
|
27
|
+
* WU-1650: Decomposed into wu-edit-validators.ts and wu-edit-operations.ts
|
|
27
28
|
* @see {@link packages/@lumenflow/cli/src/lib/micro-worktree.ts} - Shared micro-worktree logic
|
|
28
29
|
*/
|
|
29
|
-
import { getGitForCwd
|
|
30
|
+
import { getGitForCwd } from '@lumenflow/core/git-adapter';
|
|
30
31
|
import { die } from '@lumenflow/core/error-handler';
|
|
31
|
-
import {
|
|
32
|
+
import { writeFileSync } from 'node:fs';
|
|
32
33
|
import { join, resolve } from 'node:path';
|
|
33
|
-
|
|
34
|
-
// WU-1620: Import readWU for readiness summary
|
|
35
|
-
import { parseYAML, stringifyYAML, readWU } from '@lumenflow/core/wu-yaml';
|
|
34
|
+
import { stringifyYAML } from '@lumenflow/core/wu-yaml';
|
|
36
35
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
|
|
37
|
-
import { WU_PATHS
|
|
38
|
-
import {
|
|
39
|
-
import { WUStateStore } from '@lumenflow/core/wu-state-store';
|
|
40
|
-
import { FILE_SYSTEM, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI,
|
|
41
|
-
// WU-1039: Import exposure values for validation (Library-First, no magic strings)
|
|
42
|
-
WU_EXPOSURE_VALUES, } from '@lumenflow/core/wu-constants';
|
|
36
|
+
import { WU_PATHS } from '@lumenflow/core/wu-paths';
|
|
37
|
+
import { FILE_SYSTEM, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, getLaneBranch, } from '@lumenflow/core/wu-constants';
|
|
43
38
|
// WU-1593: Use centralized validateWUIDFormat (DRY)
|
|
44
39
|
import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat } from '@lumenflow/core/wu-helpers';
|
|
45
40
|
import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
|
|
46
|
-
import { validateLaneFormat } from '@lumenflow/core/lane-checker';
|
|
47
|
-
// WU-1620: Import validateSpecCompleteness for readiness summary
|
|
48
|
-
// WU-1806: Import detectCurrentWorktree for worktree path resolution
|
|
49
|
-
import { defaultWorktreeFrom, validateSpecCompleteness, detectCurrentWorktree, } from '@lumenflow/core/wu-done-validators';
|
|
50
41
|
import { validateReadyWU } from '@lumenflow/core/wu-schema';
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import { normalizeToDateString } from '@lumenflow/core/date-utils';
|
|
54
|
-
// WU-1929: Import initiative-related modules for bidirectional initiative updates
|
|
55
|
-
import { INIT_PATTERNS } from '@lumenflow/initiatives/constants';
|
|
56
|
-
import { INIT_PATHS } from '@lumenflow/initiatives/paths';
|
|
57
|
-
import { readInitiative, writeInitiative } from '@lumenflow/initiatives/yaml';
|
|
42
|
+
// WU-1806: Import detectCurrentWorktree for worktree path resolution
|
|
43
|
+
import { defaultWorktreeFrom, detectCurrentWorktree } from '@lumenflow/core/wu-done-validators';
|
|
58
44
|
// WU-2004: Import schema normalization for legacy WU formats
|
|
59
45
|
import { normalizeWUSchema } from '@lumenflow/core/wu-schema-normalization';
|
|
60
46
|
// WU-2253: Import WU spec linter for acceptance/code_paths validation
|
|
61
47
|
import { lintWUSpec, formatLintErrors } from '@lumenflow/core/wu-lint';
|
|
62
48
|
// WU-1329: Import path existence validators for strict validation
|
|
63
49
|
import { validateCodePathsExistence, validateTestPathsExistence, } from '@lumenflow/core/wu-preflight-validators';
|
|
64
|
-
import {
|
|
50
|
+
import { validateLaneFormat } from '@lumenflow/core/lane-checker';
|
|
65
51
|
import { runCLI } from './cli-entry-point.js';
|
|
66
|
-
|
|
52
|
+
// WU-1650: Import from decomposed modules
|
|
53
|
+
import { validateDoneWUEdits, validateWUEditable, validateWorktreeExists, validateWorktreeClean, validateWorktreeBranch, normalizeReplaceCodePathsArgv, EDIT_MODE, } from './wu-edit-validators.js';
|
|
54
|
+
import { applyEdits, applyEditsInWorktree, getWuEditCommitFiles, regenerateBacklogFromState, normalizeWUDates, updateInitiativeWusArrays, enforceInProgressCodePathCoverage, displayReadinessSummary, } from './wu-edit-operations.js';
|
|
55
|
+
// WU-1650: Re-export for backwards compatibility
|
|
56
|
+
// All test files and external consumers import from wu-edit.ts
|
|
57
|
+
export { validateDoneWUEdits, validateExposureValue, normalizeReplaceCodePathsArgv, hasScopeRelevantBranchChanges, } from './wu-edit-validators.js';
|
|
58
|
+
export { applyExposureEdit, applyEdits, mergeStringField, getWuEditCommitFiles, } from './wu-edit-operations.js';
|
|
67
59
|
const PREFIX = LOG_PREFIX.EDIT;
|
|
68
|
-
/**
|
|
69
|
-
* WU-1039: Validate which edits are allowed on done WUs
|
|
70
|
-
*
|
|
71
|
-
* Done WUs only allow metadata reassignment: initiative, phase, and exposure.
|
|
72
|
-
* All other edits are blocked to preserve WU immutability after completion.
|
|
73
|
-
*
|
|
74
|
-
* @param opts - Parsed CLI options
|
|
75
|
-
* @returns { valid: boolean, disallowedEdits: string[] }
|
|
76
|
-
*/
|
|
77
|
-
export function validateDoneWUEdits(opts) {
|
|
78
|
-
const disallowedEdits = [];
|
|
79
|
-
// Check for disallowed edits on done WUs
|
|
80
|
-
if (opts.specFile)
|
|
81
|
-
disallowedEdits.push('--spec-file');
|
|
82
|
-
if (opts.description)
|
|
83
|
-
disallowedEdits.push('--description');
|
|
84
|
-
if (opts.acceptance && Array.isArray(opts.acceptance) && opts.acceptance.length > 0) {
|
|
85
|
-
disallowedEdits.push('--acceptance');
|
|
86
|
-
}
|
|
87
|
-
if (opts.notes)
|
|
88
|
-
disallowedEdits.push('--notes');
|
|
89
|
-
if (opts.codePaths && Array.isArray(opts.codePaths) && opts.codePaths.length > 0) {
|
|
90
|
-
disallowedEdits.push('--code-paths');
|
|
91
|
-
}
|
|
92
|
-
if (opts.risks && Array.isArray(opts.risks) && opts.risks.length > 0) {
|
|
93
|
-
disallowedEdits.push('--risks');
|
|
94
|
-
}
|
|
95
|
-
if (opts.lane)
|
|
96
|
-
disallowedEdits.push('--lane');
|
|
97
|
-
if (opts.type)
|
|
98
|
-
disallowedEdits.push('--type');
|
|
99
|
-
if (opts.priority)
|
|
100
|
-
disallowedEdits.push('--priority');
|
|
101
|
-
if (opts.testPathsManual &&
|
|
102
|
-
Array.isArray(opts.testPathsManual) &&
|
|
103
|
-
opts.testPathsManual.length > 0) {
|
|
104
|
-
disallowedEdits.push('--test-paths-manual');
|
|
105
|
-
}
|
|
106
|
-
if (opts.testPathsUnit && Array.isArray(opts.testPathsUnit) && opts.testPathsUnit.length > 0) {
|
|
107
|
-
disallowedEdits.push('--test-paths-unit');
|
|
108
|
-
}
|
|
109
|
-
if (opts.testPathsE2e && Array.isArray(opts.testPathsE2e) && opts.testPathsE2e.length > 0) {
|
|
110
|
-
disallowedEdits.push('--test-paths-e2e');
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
valid: disallowedEdits.length === 0,
|
|
114
|
-
disallowedEdits,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* WU-1039: Validate exposure value against schema
|
|
119
|
-
*
|
|
120
|
-
* Uses WU_EXPOSURE_VALUES from core constants (Library-First, no magic strings).
|
|
121
|
-
*
|
|
122
|
-
* @param exposure - Exposure value to validate
|
|
123
|
-
* @returns { valid: boolean, error?: string }
|
|
124
|
-
*/
|
|
125
|
-
export function validateExposureValue(exposure) {
|
|
126
|
-
// WU_EXPOSURE_VALUES is readonly array, need to cast for includes check
|
|
127
|
-
const validValues = WU_EXPOSURE_VALUES;
|
|
128
|
-
if (!validValues.includes(exposure)) {
|
|
129
|
-
return {
|
|
130
|
-
valid: false,
|
|
131
|
-
error: `Invalid exposure value: "${exposure}"\n\nValid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
return { valid: true };
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* WU-1039: Apply exposure edit to WU object
|
|
138
|
-
*
|
|
139
|
-
* Returns a new WU object with updated exposure (immutable pattern).
|
|
140
|
-
*
|
|
141
|
-
* @param wu - Original WU object
|
|
142
|
-
* @param exposure - New exposure value
|
|
143
|
-
* @returns Updated WU object (does not mutate original)
|
|
144
|
-
*/
|
|
145
|
-
export function applyExposureEdit(wu, exposure) {
|
|
146
|
-
return {
|
|
147
|
-
...wu,
|
|
148
|
-
exposure,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
60
|
/**
|
|
152
61
|
* Custom options for wu-edit (not in shared WU_OPTIONS)
|
|
153
62
|
*/
|
|
@@ -262,121 +171,6 @@ const EDIT_OPTIONS = {
|
|
|
262
171
|
description: 'Replace existing dependencies instead of appending',
|
|
263
172
|
},
|
|
264
173
|
};
|
|
265
|
-
/**
|
|
266
|
-
* WU-1929: Update initiative wus: arrays bidirectionally
|
|
267
|
-
*
|
|
268
|
-
* When a WU's initiative field changes, this function:
|
|
269
|
-
* 1. Removes the WU ID from the old initiative's wus: array (if exists)
|
|
270
|
-
* 2. Adds the WU ID to the new initiative's wus: array
|
|
271
|
-
*
|
|
272
|
-
* @param {string} worktreePath - Path to the worktree (for file operations)
|
|
273
|
-
* @param {string} wuId - WU ID being updated
|
|
274
|
-
* @param {string|undefined} oldInitId - Previous initiative ID (may be undefined)
|
|
275
|
-
* @param {string} newInitId - New initiative ID
|
|
276
|
-
* @returns {Array<string>} Array of relative file paths that were modified
|
|
277
|
-
*/
|
|
278
|
-
function updateInitiativeWusArrays(worktreePath, wuId, oldInitId, newInitId) {
|
|
279
|
-
const modifiedFiles = [];
|
|
280
|
-
// Remove from old initiative if it exists and is different from new
|
|
281
|
-
if (oldInitId && oldInitId !== newInitId) {
|
|
282
|
-
const oldInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(oldInitId));
|
|
283
|
-
if (existsSync(oldInitPath)) {
|
|
284
|
-
try {
|
|
285
|
-
const oldInit = readInitiative(oldInitPath, oldInitId);
|
|
286
|
-
if (Array.isArray(oldInit.wus) && oldInit.wus.includes(wuId)) {
|
|
287
|
-
oldInit.wus = oldInit.wus.filter((id) => id !== wuId);
|
|
288
|
-
writeInitiative(oldInitPath, oldInit);
|
|
289
|
-
modifiedFiles.push(INIT_PATHS.INITIATIVE(oldInitId));
|
|
290
|
-
console.log(`${PREFIX} ✅ Removed ${wuId} from ${oldInitId} wus: array`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
// Old initiative may not exist or be invalid - log warning but continue
|
|
295
|
-
console.warn(`${PREFIX} ⚠️ Could not update old initiative ${oldInitId}: ${err.message}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// Add to new initiative
|
|
300
|
-
const newInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(newInitId));
|
|
301
|
-
if (existsSync(newInitPath)) {
|
|
302
|
-
try {
|
|
303
|
-
const newInit = readInitiative(newInitPath, newInitId);
|
|
304
|
-
if (!Array.isArray(newInit.wus)) {
|
|
305
|
-
newInit.wus = [];
|
|
306
|
-
}
|
|
307
|
-
if (!newInit.wus.includes(wuId)) {
|
|
308
|
-
newInit.wus.push(wuId);
|
|
309
|
-
writeInitiative(newInitPath, newInit);
|
|
310
|
-
modifiedFiles.push(INIT_PATHS.INITIATIVE(newInitId));
|
|
311
|
-
console.log(`${PREFIX} ✅ Added ${wuId} to ${newInitId} wus: array`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
die(`Failed to update new initiative ${newInitId}: ${err.message}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return modifiedFiles;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* WU-1929: Validate initiative ID format
|
|
322
|
-
* @param {string} initId - Initiative ID to validate
|
|
323
|
-
*/
|
|
324
|
-
function validateInitiativeFormat(initId) {
|
|
325
|
-
if (!INIT_PATTERNS.INIT_ID.test(initId)) {
|
|
326
|
-
die(`Invalid Initiative ID format: "${initId}"\n\n` +
|
|
327
|
-
`Expected format: INIT-<number> or INIT-<NAME> (e.g., INIT-001, INIT-TOOLING)`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* WU-1929: Validate initiative exists on disk
|
|
332
|
-
* @param {string} initId - Initiative ID to check
|
|
333
|
-
* @returns {string} Path to initiative file
|
|
334
|
-
*/
|
|
335
|
-
function validateInitiativeExists(initId) {
|
|
336
|
-
const initPath = INIT_PATHS.INITIATIVE(initId);
|
|
337
|
-
if (!existsSync(initPath)) {
|
|
338
|
-
die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
|
|
339
|
-
}
|
|
340
|
-
return initPath;
|
|
341
|
-
}
|
|
342
|
-
const NON_SCOPE_RELEVANT_PATHS = new Set([
|
|
343
|
-
'.lumenflow/state/wu-events.jsonl',
|
|
344
|
-
'docs/04-operations/tasks/backlog.md',
|
|
345
|
-
'docs/04-operations/tasks/status.md',
|
|
346
|
-
]);
|
|
347
|
-
/**
|
|
348
|
-
* WU-1618: Treat backlog/state bookkeeping files as non-scope signals.
|
|
349
|
-
*/
|
|
350
|
-
export function hasScopeRelevantBranchChanges(changedFiles) {
|
|
351
|
-
return changedFiles.some((filePath) => {
|
|
352
|
-
const normalized = filePath.trim().replace(/\\/g, '/');
|
|
353
|
-
if (!normalized) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
if (NON_SCOPE_RELEVANT_PATHS.has(normalized)) {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
return !normalized.startsWith('docs/04-operations/tasks/wu/');
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* WU-1618: Support `--replace-code-paths <paths>` shorthand by normalizing to
|
|
364
|
-
* `--replace-code-paths --code-paths <paths>` before Commander parsing.
|
|
365
|
-
*/
|
|
366
|
-
export function normalizeReplaceCodePathsArgv(argv) {
|
|
367
|
-
const normalized = [...argv];
|
|
368
|
-
for (let i = 0; i < normalized.length; i += 1) {
|
|
369
|
-
if (normalized[i] !== '--replace-code-paths') {
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const next = normalized[i + 1];
|
|
373
|
-
if (next && !next.startsWith('-')) {
|
|
374
|
-
normalized.splice(i + 1, 0, '--code-paths');
|
|
375
|
-
i += 1;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return normalized;
|
|
379
|
-
}
|
|
380
174
|
/**
|
|
381
175
|
* Parse command line arguments
|
|
382
176
|
*/
|
|
@@ -432,260 +226,6 @@ function parseArgs() {
|
|
|
432
226
|
process.argv = originalArgv;
|
|
433
227
|
}
|
|
434
228
|
}
|
|
435
|
-
function enforceInProgressCodePathCoverage(options) {
|
|
436
|
-
const { id, editOpts, codePaths = [], cwd } = options;
|
|
437
|
-
const hasCodePathEdit = Array.isArray(editOpts.codePaths)
|
|
438
|
-
? editOpts.codePaths.length > 0
|
|
439
|
-
: Boolean(editOpts.codePaths);
|
|
440
|
-
if (!hasCodePathEdit) {
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (!Array.isArray(codePaths) || codePaths.length === 0) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const coverage = checkCodePathCoverageBeforeGates({
|
|
447
|
-
wuId: id,
|
|
448
|
-
codePaths,
|
|
449
|
-
cwd,
|
|
450
|
-
});
|
|
451
|
-
if (coverage.valid) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (!hasScopeRelevantBranchChanges(coverage.changedFiles)) {
|
|
455
|
-
console.warn(`${PREFIX} ⚠️ code_paths coverage check deferred (no scope-relevant branch changes yet).`);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
die(`${formatCodePathCoverageFailure({
|
|
459
|
-
wuId: id,
|
|
460
|
-
missingCodePaths: coverage.missingCodePaths,
|
|
461
|
-
changedFiles: coverage.changedFiles,
|
|
462
|
-
error: coverage.error,
|
|
463
|
-
})}\n\n` +
|
|
464
|
-
`${PREFIX} Tip: if you're still defining scope before code changes, run wu:edit again after your first scope-relevant commit.`);
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* WU-1620: Display readiness summary after edit
|
|
468
|
-
*
|
|
469
|
-
* Shows whether WU is ready for wu:claim based on spec completeness.
|
|
470
|
-
* Non-blocking - just informational to help agents understand what's missing.
|
|
471
|
-
*
|
|
472
|
-
* @param {string} id - WU ID
|
|
473
|
-
*/
|
|
474
|
-
function displayReadinessSummary(id) {
|
|
475
|
-
try {
|
|
476
|
-
const wuPath = WU_PATHS.WU(id);
|
|
477
|
-
const wuDoc = readWU(wuPath, id);
|
|
478
|
-
const { valid, errors } = validateSpecCompleteness(wuDoc, id);
|
|
479
|
-
const { BOX, BOX_WIDTH, MESSAGES, ERROR_MAX_LENGTH, ERROR_TRUNCATE_LENGTH, TRUNCATION_SUFFIX, PADDING, } = READINESS_UI;
|
|
480
|
-
console.log(`\n${BOX.TOP_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.TOP_RIGHT}`);
|
|
481
|
-
if (valid) {
|
|
482
|
-
console.log(`${BOX.VERTICAL} ${MESSAGES.READY_YES}${''.padEnd(PADDING.READY_YES)}${BOX.VERTICAL}`);
|
|
483
|
-
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
484
|
-
const claimCmd = `Run: pnpm wu:claim --id ${id}`;
|
|
485
|
-
console.log(`${BOX.VERTICAL} ${claimCmd}${''.padEnd(BOX_WIDTH - claimCmd.length - 1)}${BOX.VERTICAL}`);
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
console.log(`${BOX.VERTICAL} ${MESSAGES.READY_NO}${''.padEnd(PADDING.READY_NO)}${BOX.VERTICAL}`);
|
|
489
|
-
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
490
|
-
console.log(`${BOX.VERTICAL} ${MESSAGES.MISSING_HEADER}${''.padEnd(PADDING.MISSING_HEADER)}${BOX.VERTICAL}`);
|
|
491
|
-
for (const error of errors) {
|
|
492
|
-
// Truncate long error messages to fit box
|
|
493
|
-
const truncated = error.length > ERROR_MAX_LENGTH
|
|
494
|
-
? `${error.substring(0, ERROR_TRUNCATE_LENGTH)}${TRUNCATION_SUFFIX}`
|
|
495
|
-
: error;
|
|
496
|
-
console.log(`${BOX.VERTICAL} ${MESSAGES.BULLET} ${truncated}${''.padEnd(Math.max(0, PADDING.ERROR_BULLET - truncated.length))}${BOX.VERTICAL}`);
|
|
497
|
-
}
|
|
498
|
-
console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
|
|
499
|
-
const editCmd = `Run: pnpm wu:edit --id ${id} --help`;
|
|
500
|
-
console.log(`${BOX.VERTICAL} ${editCmd}${''.padEnd(BOX_WIDTH - editCmd.length - 1)}${BOX.VERTICAL}`);
|
|
501
|
-
}
|
|
502
|
-
console.log(`${BOX.BOTTOM_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.BOTTOM_RIGHT}`);
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
// Non-blocking - if validation fails, just warn
|
|
506
|
-
console.warn(`${PREFIX} ⚠️ Could not validate readiness: ${err.message}`);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Edit modes for WU editing
|
|
511
|
-
* WU-1365: Worktree-aware editing support
|
|
512
|
-
*/
|
|
513
|
-
const EDIT_MODE = {
|
|
514
|
-
/** Ready WUs: Use micro-worktree on main (existing behavior) */
|
|
515
|
-
MICRO_WORKTREE: 'micro_worktree',
|
|
516
|
-
/** In-progress worktree WUs: Apply edits directly in active worktree (WU-1365) */
|
|
517
|
-
WORKTREE: 'worktree',
|
|
518
|
-
/** In-progress branch-pr WUs: apply edits directly on the claimed branch */
|
|
519
|
-
BRANCH_PR: BRANCH_PR_EDIT_MODE,
|
|
520
|
-
};
|
|
521
|
-
/**
|
|
522
|
-
* Normalize date fields in WU object to prevent date corruption
|
|
523
|
-
*
|
|
524
|
-
* WU-1442: js-yaml parses unquoted YYYY-MM-DD dates as Date objects.
|
|
525
|
-
* When yaml.dump() serializes them back, it outputs ISO timestamps.
|
|
526
|
-
* This function normalizes Date objects back to YYYY-MM-DD strings.
|
|
527
|
-
*
|
|
528
|
-
* @param {object} wu - WU object from yaml.load()
|
|
529
|
-
* @returns {object} WU object with normalized date fields
|
|
530
|
-
*/
|
|
531
|
-
function normalizeWUDates(wu) {
|
|
532
|
-
if (wu.created !== undefined) {
|
|
533
|
-
wu.created = normalizeToDateString(wu.created);
|
|
534
|
-
}
|
|
535
|
-
return wu;
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Check WU exists and determine edit mode
|
|
539
|
-
* WU-1365: Now supports worktree-aware editing for in_progress WUs
|
|
540
|
-
*
|
|
541
|
-
* @param {string} id - WU ID
|
|
542
|
-
* @returns {{ wu: object, editMode: string }} WU object and edit mode
|
|
543
|
-
*/
|
|
544
|
-
function validateWUEditable(id) {
|
|
545
|
-
const wuPath = WU_PATHS.WU(id);
|
|
546
|
-
if (!existsSync(wuPath)) {
|
|
547
|
-
die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
|
|
548
|
-
}
|
|
549
|
-
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.ENCODING });
|
|
550
|
-
const wu = parseYAML(content);
|
|
551
|
-
// WU-1929: Done WUs allow initiative/phase edits only (metadata reassignment)
|
|
552
|
-
// WU-1365: Other fields on done WUs are immutable
|
|
553
|
-
if (wu.status === WU_STATUS.DONE) {
|
|
554
|
-
// Return done status - main() will validate allowed fields
|
|
555
|
-
return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: true };
|
|
556
|
-
}
|
|
557
|
-
// Handle in_progress WUs based on claimed_mode (WU-1365)
|
|
558
|
-
if (wu.status === WU_STATUS.IN_PROGRESS) {
|
|
559
|
-
const editMode = resolveInProgressEditMode(typeof wu.claimed_mode === 'string' ? wu.claimed_mode : undefined);
|
|
560
|
-
if (editMode === BLOCKED_EDIT_MODE) {
|
|
561
|
-
die(`Cannot edit branch-only WU ${id} via wu:edit.\n\n` +
|
|
562
|
-
`WUs claimed with claimed_mode='branch-only' cannot be edited via wu:edit.\n` +
|
|
563
|
-
`To modify the spec, edit the file directly on the lane branch and commit.`);
|
|
564
|
-
}
|
|
565
|
-
if (editMode === EDIT_MODE.BRANCH_PR) {
|
|
566
|
-
return { wu, editMode: EDIT_MODE.BRANCH_PR, isDone: false };
|
|
567
|
-
}
|
|
568
|
-
return { wu, editMode: EDIT_MODE.WORKTREE, isDone: false };
|
|
569
|
-
}
|
|
570
|
-
// Ready WUs use micro-worktree (existing behavior)
|
|
571
|
-
if (wu.status === WU_STATUS.READY) {
|
|
572
|
-
return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: false };
|
|
573
|
-
}
|
|
574
|
-
// Block other statuses (blocked, etc.)
|
|
575
|
-
die(`Cannot edit WU ${id}: status is '${wu.status}'.\n\n` +
|
|
576
|
-
`Only WUs in '${WU_STATUS.READY}' or '${WU_STATUS.IN_PROGRESS}' (worktree mode) can be edited.`);
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Validate worktree exists on disk
|
|
580
|
-
* WU-1365: Required check before worktree editing
|
|
581
|
-
*
|
|
582
|
-
* @param {string} worktreePath - Absolute path to worktree
|
|
583
|
-
* @param {string} id - WU ID (for error messages)
|
|
584
|
-
*/
|
|
585
|
-
function validateWorktreeExists(worktreePath, id) {
|
|
586
|
-
if (!existsSync(worktreePath)) {
|
|
587
|
-
die(`Cannot edit WU ${id}: worktree path missing from disk.\n\n` +
|
|
588
|
-
`Expected worktree at: ${worktreePath}\n\n` +
|
|
589
|
-
`The worktree may have been removed or the path is incorrect.\n` +
|
|
590
|
-
`If the worktree was accidentally deleted, you may need to re-claim the WU.`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Validate worktree has no uncommitted changes
|
|
595
|
-
* WU-1365: Required check to prevent edit conflicts
|
|
596
|
-
*
|
|
597
|
-
* @param {string} worktreePath - Absolute path to worktree
|
|
598
|
-
* @param {string} id - WU ID (for error messages)
|
|
599
|
-
*/
|
|
600
|
-
async function validateWorktreeClean(worktreePath, id) {
|
|
601
|
-
try {
|
|
602
|
-
const gitAdapter = createGitForPath(worktreePath);
|
|
603
|
-
const status = (await gitAdapter.raw(['status', '--porcelain'])).trim();
|
|
604
|
-
if (status !== '') {
|
|
605
|
-
die(`Cannot edit WU ${id}: worktree has uncommitted changes.\n\n` +
|
|
606
|
-
`Uncommitted changes in ${worktreePath}:\n${status}\n\n` +
|
|
607
|
-
`Commit or discard your changes before editing the WU spec:\n` +
|
|
608
|
-
` cd ${worktreePath}\n` +
|
|
609
|
-
` git add . && git commit -m "wip: save progress"\n\n` +
|
|
610
|
-
`Then retry wu:edit.`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
catch (err) {
|
|
614
|
-
die(`Cannot edit WU ${id}: failed to check worktree status.\n\n` +
|
|
615
|
-
`Error: ${err.message}\n\n` +
|
|
616
|
-
`Worktree path: ${worktreePath}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Validate worktree is on expected lane branch
|
|
621
|
-
* WU-1365: Prevents editing WUs in worktrees with mismatched branches
|
|
622
|
-
*
|
|
623
|
-
* @param {string} worktreePath - Absolute path to worktree
|
|
624
|
-
* @param {string} expectedBranch - Expected branch name (e.g., lane/operations-tooling/wu-1365)
|
|
625
|
-
* @param {string} id - WU ID (for error messages)
|
|
626
|
-
*/
|
|
627
|
-
async function validateWorktreeBranch(worktreePath, expectedBranch, id) {
|
|
628
|
-
try {
|
|
629
|
-
const gitAdapter = createGitForPath(worktreePath);
|
|
630
|
-
const actualBranch = (await gitAdapter.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
631
|
-
if (actualBranch !== expectedBranch) {
|
|
632
|
-
die(`Cannot edit WU ${id}: worktree branch does not match expected lane branch.\n\n` +
|
|
633
|
-
`Expected branch: ${expectedBranch}\n` +
|
|
634
|
-
`Actual branch: ${actualBranch}\n\n` +
|
|
635
|
-
`This may indicate a corrupted worktree state.\n` +
|
|
636
|
-
`Verify the worktree is correctly set up for this WU.`);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
catch (err) {
|
|
640
|
-
die(`Cannot edit WU ${id}: failed to check worktree branch.\n\n` +
|
|
641
|
-
`Error: ${err.message}\n\n` +
|
|
642
|
-
`Worktree path: ${worktreePath}`);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Apply edits directly in an active worktree (WU-1365)
|
|
647
|
-
* Used for in_progress WUs with claimed_mode=worktree
|
|
648
|
-
*
|
|
649
|
-
* @param {object} params - Parameters
|
|
650
|
-
* @param {string} params.worktreePath - Absolute path to worktree
|
|
651
|
-
* @param {string} params.id - WU ID
|
|
652
|
-
* @param {object} params.updatedWU - Updated WU object
|
|
653
|
-
*/
|
|
654
|
-
async function applyEditsInWorktree({ worktreePath, id, updatedWU }) {
|
|
655
|
-
const wuPath = join(worktreePath, WU_PATHS.WU(id));
|
|
656
|
-
// WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
|
|
657
|
-
normalizeWUDates(updatedWU);
|
|
658
|
-
// Emergency fix Session 2: Use centralized stringifyYAML helper
|
|
659
|
-
const yamlContent = stringifyYAML(updatedWU);
|
|
660
|
-
writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
661
|
-
console.log(`${PREFIX} ✅ Updated ${id}.yaml in worktree`);
|
|
662
|
-
// Format the file
|
|
663
|
-
try {
|
|
664
|
-
execSync(`${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} "${wuPath}"`, {
|
|
665
|
-
cwd: worktreePath,
|
|
666
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
667
|
-
stdio: 'pipe',
|
|
668
|
-
});
|
|
669
|
-
console.log(`${PREFIX} ✅ Formatted ${id}.yaml`);
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
console.warn(`${PREFIX} ⚠️ Could not format file: ${err.message}`);
|
|
673
|
-
}
|
|
674
|
-
// Stage and commit using git adapter (library-first)
|
|
675
|
-
const commitMsg = COMMIT_FORMATS.SPEC_UPDATE(id);
|
|
676
|
-
try {
|
|
677
|
-
const gitAdapter = createGitForPath(worktreePath);
|
|
678
|
-
await gitAdapter.add(wuPath);
|
|
679
|
-
await gitAdapter.commit(commitMsg);
|
|
680
|
-
console.log(`${PREFIX} ✅ Committed: ${commitMsg}`);
|
|
681
|
-
}
|
|
682
|
-
catch (err) {
|
|
683
|
-
die(`Failed to commit edit in worktree.\n\n` +
|
|
684
|
-
`Error: ${err.message}\n\n` +
|
|
685
|
-
`The WU file was updated but could not be committed.\n` +
|
|
686
|
-
`You may need to commit manually in the worktree.`);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
229
|
/**
|
|
690
230
|
* Ensure working tree is clean
|
|
691
231
|
*/
|
|
@@ -695,232 +235,6 @@ async function ensureCleanWorkingTree() {
|
|
|
695
235
|
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`);
|
|
696
236
|
}
|
|
697
237
|
}
|
|
698
|
-
/**
|
|
699
|
-
* Merge array values: replace by default, append if --append flag is set (WU-1388)
|
|
700
|
-
* @param {Array} existing - Current array value from WU
|
|
701
|
-
* @param {Array} newValues - New values from CLI
|
|
702
|
-
* @param {boolean} shouldAppend - Whether to append instead of replace
|
|
703
|
-
* @returns {Array} Merged array
|
|
704
|
-
*/
|
|
705
|
-
function mergeArrayField(existing, newValues, shouldAppend) {
|
|
706
|
-
if (!shouldAppend) {
|
|
707
|
-
return newValues;
|
|
708
|
-
}
|
|
709
|
-
const existingArray = Array.isArray(existing) ? existing : [];
|
|
710
|
-
return [...existingArray, ...newValues];
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* WU-1144: Merge string field values with append-by-default behavior
|
|
714
|
-
*
|
|
715
|
-
* Notes and acceptance criteria should append by default (preserving original),
|
|
716
|
-
* with explicit --replace-notes and --replace-acceptance flags for overwrite.
|
|
717
|
-
*
|
|
718
|
-
* @param {string | undefined} existing - Current string value from WU
|
|
719
|
-
* @param {string} newValue - New value from CLI
|
|
720
|
-
* @param {boolean} shouldReplace - Whether to replace instead of append
|
|
721
|
-
* @returns {string} Merged string value
|
|
722
|
-
*/
|
|
723
|
-
export function mergeStringField(existing, newValue, shouldReplace) {
|
|
724
|
-
// If replace mode or no existing value, just use new value
|
|
725
|
-
if (shouldReplace || !existing || existing.trim() === '') {
|
|
726
|
-
return newValue;
|
|
727
|
-
}
|
|
728
|
-
// Append with double newline separator
|
|
729
|
-
return `${existing}\n\n${newValue}`;
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* WU-1594: Ensure wu:edit commits always include regenerated backlog projection.
|
|
733
|
-
*
|
|
734
|
-
* @param {string} id - WU ID
|
|
735
|
-
* @param {string[]} extraFiles - Additional files modified during edit
|
|
736
|
-
* @returns {string[]} Deduplicated list of files for commit
|
|
737
|
-
*/
|
|
738
|
-
export function getWuEditCommitFiles(id, extraFiles = []) {
|
|
739
|
-
return [...new Set([WU_PATHS.WU(id), ...extraFiles, WU_PATHS.BACKLOG()])];
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* WU-1594: Regenerate backlog.md from state store after wu:edit updates.
|
|
743
|
-
*
|
|
744
|
-
* @param {string} backlogPath - Absolute path to backlog.md in micro-worktree
|
|
745
|
-
*/
|
|
746
|
-
async function regenerateBacklogFromState(backlogPath) {
|
|
747
|
-
const stateDir = getStateStoreDirFromBacklog(backlogPath);
|
|
748
|
-
const store = new WUStateStore(stateDir);
|
|
749
|
-
await store.load();
|
|
750
|
-
const content = await generateBacklog(store);
|
|
751
|
-
writeFileSync(backlogPath, content, { encoding: FILE_SYSTEM.ENCODING });
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Load spec file and merge with original WU (preserving id and status)
|
|
755
|
-
* @param {string} specPath - Path to spec file
|
|
756
|
-
* @param {object} originalWU - Original WU object
|
|
757
|
-
* @returns {object} Merged WU object
|
|
758
|
-
*/
|
|
759
|
-
function loadSpecFile(specPath, originalWU) {
|
|
760
|
-
const resolvedPath = resolve(specPath);
|
|
761
|
-
if (!existsSync(resolvedPath)) {
|
|
762
|
-
die(`Spec file not found: ${resolvedPath}`);
|
|
763
|
-
}
|
|
764
|
-
const specContent = readFileSync(resolvedPath, {
|
|
765
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
766
|
-
});
|
|
767
|
-
const newSpec = parseYAML(specContent);
|
|
768
|
-
// Preserve id and status from original (cannot be changed via edit)
|
|
769
|
-
return {
|
|
770
|
-
...newSpec,
|
|
771
|
-
id: originalWU.id,
|
|
772
|
-
status: originalWU.status,
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Apply edits to WU YAML
|
|
777
|
-
* Returns the updated WU object
|
|
778
|
-
*/
|
|
779
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
780
|
-
export function applyEdits(wu, opts) {
|
|
781
|
-
// Full spec replacement from file
|
|
782
|
-
if (opts.specFile) {
|
|
783
|
-
return loadSpecFile(opts.specFile, wu);
|
|
784
|
-
}
|
|
785
|
-
const updated = { ...wu };
|
|
786
|
-
// Field-level updates
|
|
787
|
-
if (opts.description) {
|
|
788
|
-
updated.description = opts.description;
|
|
789
|
-
}
|
|
790
|
-
// WU-1144: Handle --acceptance with append-by-default behavior
|
|
791
|
-
// Appends to existing acceptance criteria unless --replace-acceptance is set
|
|
792
|
-
if (opts.acceptance && opts.acceptance.length > 0) {
|
|
793
|
-
// Invert the logic: append by default, replace with --replace-acceptance
|
|
794
|
-
const shouldAppend = !opts.replaceAcceptance;
|
|
795
|
-
updated.acceptance = mergeArrayField(wu.acceptance, opts.acceptance, shouldAppend);
|
|
796
|
-
}
|
|
797
|
-
// WU-1144: Handle --notes with append-by-default behavior
|
|
798
|
-
// Appends to existing notes unless --replace-notes is set
|
|
799
|
-
if (opts.notes) {
|
|
800
|
-
updated.notes = mergeStringField(wu.notes, opts.notes, opts.replaceNotes ?? false);
|
|
801
|
-
}
|
|
802
|
-
// WU-1456: Handle lane reassignment
|
|
803
|
-
if (opts.lane) {
|
|
804
|
-
validateLaneFormat(opts.lane);
|
|
805
|
-
updated.lane = opts.lane;
|
|
806
|
-
}
|
|
807
|
-
// WU-1620: Handle type and priority updates
|
|
808
|
-
if (opts.type) {
|
|
809
|
-
updated.type = opts.type;
|
|
810
|
-
}
|
|
811
|
-
if (opts.priority) {
|
|
812
|
-
updated.priority = opts.priority;
|
|
813
|
-
}
|
|
814
|
-
// WU-1929: Handle initiative and phase updates
|
|
815
|
-
// Note: Initiative bidirectional updates (initiative wus: arrays) are handled separately
|
|
816
|
-
// in the main function after applyEdits, since they require file I/O
|
|
817
|
-
if (opts.initiative) {
|
|
818
|
-
validateInitiativeFormat(opts.initiative);
|
|
819
|
-
validateInitiativeExists(opts.initiative);
|
|
820
|
-
updated.initiative = opts.initiative;
|
|
821
|
-
}
|
|
822
|
-
if (opts.phase !== undefined && opts.phase !== null) {
|
|
823
|
-
const phaseNum = parseInt(opts.phase, 10);
|
|
824
|
-
if (isNaN(phaseNum) || phaseNum < 1) {
|
|
825
|
-
die(`Invalid phase number: "${opts.phase}"\n\nPhase must be a positive integer (e.g., 1, 2, 3)`);
|
|
826
|
-
}
|
|
827
|
-
updated.phase = phaseNum;
|
|
828
|
-
}
|
|
829
|
-
// Handle repeatable --code-paths flags (WU-1225: append by default, replace with --replace-code-paths)
|
|
830
|
-
// WU-1816: Split comma-separated string into array (same pattern as test paths)
|
|
831
|
-
// WU-1870: Fix to split comma-separated values WITHIN array elements (Commander passes ['a,b'] not 'a,b')
|
|
832
|
-
if (opts.codePaths && opts.codePaths.length > 0) {
|
|
833
|
-
const rawCodePaths = opts.codePaths;
|
|
834
|
-
const codePaths = Array.isArray(rawCodePaths)
|
|
835
|
-
? rawCodePaths
|
|
836
|
-
.flatMap((p) => p.split(','))
|
|
837
|
-
.map((p) => p.trim())
|
|
838
|
-
.filter(Boolean)
|
|
839
|
-
: rawCodePaths
|
|
840
|
-
.split(',')
|
|
841
|
-
.map((p) => p.trim())
|
|
842
|
-
.filter(Boolean);
|
|
843
|
-
// WU-1225: Invert logic - append by default, replace with --replace-code-paths
|
|
844
|
-
// Also support legacy --append flag for backwards compatibility
|
|
845
|
-
const shouldAppend = !opts.replaceCodePaths || opts.append;
|
|
846
|
-
updated.code_paths = mergeArrayField(wu.code_paths, codePaths, shouldAppend);
|
|
847
|
-
}
|
|
848
|
-
// WU-1225: Handle repeatable --risks flags (append by default, replace with --replace-risks)
|
|
849
|
-
// Split comma-separated values within each entry for consistency with other list fields
|
|
850
|
-
if (opts.risks && opts.risks.length > 0) {
|
|
851
|
-
const rawRisks = opts.risks;
|
|
852
|
-
const risks = Array.isArray(rawRisks)
|
|
853
|
-
? rawRisks
|
|
854
|
-
.flatMap((risk) => risk.split(','))
|
|
855
|
-
.map((risk) => risk.trim())
|
|
856
|
-
.filter(Boolean)
|
|
857
|
-
: rawRisks
|
|
858
|
-
.split(',')
|
|
859
|
-
.map((risk) => risk.trim())
|
|
860
|
-
.filter(Boolean);
|
|
861
|
-
// WU-1225: Invert logic - append by default
|
|
862
|
-
const shouldAppend = !opts.replaceRisks || opts.append;
|
|
863
|
-
updated.risks = mergeArrayField(wu.risks, risks, shouldAppend);
|
|
864
|
-
}
|
|
865
|
-
// WU-1390: Handle test path flags (DRY refactor)
|
|
866
|
-
// WU-1225: Test paths now append by default (consistent with --acceptance and --code-paths)
|
|
867
|
-
const testPathMappings = [
|
|
868
|
-
{ optKey: 'testPathsManual', field: 'manual' },
|
|
869
|
-
{ optKey: 'testPathsUnit', field: 'unit' },
|
|
870
|
-
{ optKey: 'testPathsE2e', field: 'e2e' },
|
|
871
|
-
];
|
|
872
|
-
for (const { optKey, field } of testPathMappings) {
|
|
873
|
-
const rawPaths = opts[optKey];
|
|
874
|
-
if (rawPaths && rawPaths.length > 0) {
|
|
875
|
-
// Split comma-separated string into array (options are comma-separated per description)
|
|
876
|
-
// WU-1870: Fix to split comma-separated values WITHIN array elements
|
|
877
|
-
const paths = Array.isArray(rawPaths)
|
|
878
|
-
? rawPaths
|
|
879
|
-
.flatMap((p) => p.split(','))
|
|
880
|
-
.map((p) => p.trim())
|
|
881
|
-
.filter(Boolean)
|
|
882
|
-
: rawPaths
|
|
883
|
-
.split(',')
|
|
884
|
-
.map((p) => p.trim())
|
|
885
|
-
.filter(Boolean);
|
|
886
|
-
updated.tests = updated.tests || {};
|
|
887
|
-
// WU-1225: Append by default (no individual replace flags for test paths yet)
|
|
888
|
-
const shouldAppend = true;
|
|
889
|
-
updated.tests[field] = mergeArrayField(wu.tests?.[field], paths, shouldAppend);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
// WU-2564: Handle --blocked-by flag
|
|
893
|
-
// WU-1225: Append by default, replace with --replace-blocked-by
|
|
894
|
-
if (opts.blockedBy) {
|
|
895
|
-
const rawBlockedBy = opts.blockedBy;
|
|
896
|
-
const blockedByIds = rawBlockedBy
|
|
897
|
-
.split(',')
|
|
898
|
-
.map((id) => id.trim())
|
|
899
|
-
.filter(Boolean);
|
|
900
|
-
const shouldAppend = !opts.replaceBlockedBy || opts.append;
|
|
901
|
-
updated.blocked_by = mergeArrayField(wu.blocked_by, blockedByIds, shouldAppend);
|
|
902
|
-
}
|
|
903
|
-
// WU-2564: Handle --add-dep flag
|
|
904
|
-
// WU-1225: Append by default, replace with --replace-dependencies
|
|
905
|
-
if (opts.addDep) {
|
|
906
|
-
const rawAddDep = opts.addDep;
|
|
907
|
-
const depIds = rawAddDep
|
|
908
|
-
.split(',')
|
|
909
|
-
.map((id) => id.trim())
|
|
910
|
-
.filter(Boolean);
|
|
911
|
-
const shouldAppend = !opts.replaceDependencies || opts.append;
|
|
912
|
-
updated.dependencies = mergeArrayField(wu.dependencies, depIds, shouldAppend);
|
|
913
|
-
}
|
|
914
|
-
// WU-1039: Handle --exposure flag with validation
|
|
915
|
-
if (opts.exposure) {
|
|
916
|
-
const exposureResult = validateExposureValue(opts.exposure);
|
|
917
|
-
if (!exposureResult.valid) {
|
|
918
|
-
die(exposureResult.error);
|
|
919
|
-
}
|
|
920
|
-
updated.exposure = opts.exposure;
|
|
921
|
-
}
|
|
922
|
-
return updated;
|
|
923
|
-
}
|
|
924
238
|
/**
|
|
925
239
|
* Main entry point
|
|
926
240
|
*/
|
|
@@ -1003,16 +317,16 @@ async function main() {
|
|
|
1003
317
|
// Apply edits to get updated WU
|
|
1004
318
|
const updatedWU = applyEdits(originalWU, opts);
|
|
1005
319
|
// WU-2004: Normalize legacy schema fields before validation
|
|
1006
|
-
// Converts: summary
|
|
320
|
+
// Converts: summary->description, string risks->array, test_paths->tests, etc.
|
|
1007
321
|
const normalizedForValidation = normalizeWUSchema(updatedWU);
|
|
1008
322
|
// WU-1539: Validate WU structure after applying edits (fail-fast, allows placeholders)
|
|
1009
323
|
// WU-1750: Zod transforms normalize embedded newlines in arrays and strings
|
|
1010
324
|
const validationResult = validateReadyWU(normalizedForValidation);
|
|
1011
325
|
if (!validationResult.success) {
|
|
1012
326
|
const errors = validationResult.error.issues
|
|
1013
|
-
.map((issue) => `
|
|
327
|
+
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
|
|
1014
328
|
.join('\n');
|
|
1015
|
-
die(`${PREFIX}
|
|
329
|
+
die(`${PREFIX} WU YAML validation failed:\n\n${errors}\n\nFix the issues above and retry.`);
|
|
1016
330
|
}
|
|
1017
331
|
// WU-2253: Validate acceptance/code_paths consistency and invariants compliance
|
|
1018
332
|
// This blocks WU edits if acceptance references paths not in code_paths
|
|
@@ -1021,7 +335,7 @@ async function main() {
|
|
|
1021
335
|
const lintResult = lintWUSpec(normalizedForValidation, { invariantsPath });
|
|
1022
336
|
if (!lintResult.valid) {
|
|
1023
337
|
const formatted = formatLintErrors(lintResult.errors);
|
|
1024
|
-
die(`${PREFIX}
|
|
338
|
+
die(`${PREFIX} WU SPEC LINT FAILED:\n\n${formatted}\n` +
|
|
1025
339
|
`Fix the issues above before editing this WU.`);
|
|
1026
340
|
}
|
|
1027
341
|
// WU-1750: CRITICAL - Use transformed data for all subsequent operations
|
|
@@ -1051,8 +365,8 @@ async function main() {
|
|
|
1051
365
|
}
|
|
1052
366
|
}
|
|
1053
367
|
if (strictErrors.length > 0) {
|
|
1054
|
-
const errorList = strictErrors.map((e) => `
|
|
1055
|
-
die(`${PREFIX}
|
|
368
|
+
const errorList = strictErrors.map((e) => ` - ${e}`).join('\n');
|
|
369
|
+
die(`${PREFIX} Strict validation failed:\n\n${errorList}\n\n` +
|
|
1056
370
|
`Options:\n` +
|
|
1057
371
|
` 1. Fix the paths in the WU spec to match actual files\n` +
|
|
1058
372
|
` 2. Use --no-strict to bypass path existence checks (not recommended)`);
|
|
@@ -1100,7 +414,7 @@ async function main() {
|
|
|
1100
414
|
updatedWU: normalizedWU,
|
|
1101
415
|
});
|
|
1102
416
|
await getGitForCwd().push('origin', currentBranch);
|
|
1103
|
-
console.log(`${PREFIX}
|
|
417
|
+
console.log(`${PREFIX} Successfully edited ${id} on branch ${currentBranch}`);
|
|
1104
418
|
console.log(`${PREFIX} Changes committed and pushed to origin/${currentBranch}`);
|
|
1105
419
|
displayReadinessSummary(id);
|
|
1106
420
|
}
|
|
@@ -1158,7 +472,7 @@ async function main() {
|
|
|
1158
472
|
id,
|
|
1159
473
|
updatedWU: normalizedWU,
|
|
1160
474
|
});
|
|
1161
|
-
console.log(`${PREFIX}
|
|
475
|
+
console.log(`${PREFIX} Successfully edited ${id} in worktree`);
|
|
1162
476
|
console.log(`${PREFIX} Changes committed to lane branch`);
|
|
1163
477
|
// WU-1620: Display readiness summary
|
|
1164
478
|
displayReadinessSummary(id);
|
|
@@ -1190,7 +504,7 @@ async function main() {
|
|
|
1190
504
|
// Emergency fix Session 2: Use centralized stringifyYAML helper
|
|
1191
505
|
const yamlContent = stringifyYAML(normalizedWU);
|
|
1192
506
|
writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
1193
|
-
console.log(`${PREFIX}
|
|
507
|
+
console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
|
|
1194
508
|
// WU-1929: Handle bidirectional initiative updates
|
|
1195
509
|
if (initiativeChanged) {
|
|
1196
510
|
const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
|
|
@@ -1199,7 +513,7 @@ async function main() {
|
|
|
1199
513
|
// WU-1594: Keep backlog projection synchronized with WU lane/spec edits.
|
|
1200
514
|
const backlogPath = join(worktreePath, WU_PATHS.BACKLOG());
|
|
1201
515
|
await regenerateBacklogFromState(backlogPath);
|
|
1202
|
-
console.log(`${PREFIX}
|
|
516
|
+
console.log(`${PREFIX} Regenerated backlog.md in micro-worktree`);
|
|
1203
517
|
return {
|
|
1204
518
|
commitMessage: COMMIT_FORMATS.EDIT(id),
|
|
1205
519
|
files: getWuEditCommitFiles(id, extraFiles),
|
|
@@ -1215,7 +529,7 @@ async function main() {
|
|
|
1215
529
|
process.env.LUMENFLOW_WU_TOOL = previousWuTool;
|
|
1216
530
|
}
|
|
1217
531
|
}
|
|
1218
|
-
console.log(`${PREFIX}
|
|
532
|
+
console.log(`${PREFIX} Successfully edited ${id}`);
|
|
1219
533
|
console.log(`${PREFIX} Changes pushed to origin/main`);
|
|
1220
534
|
// WU-1620: Display readiness summary
|
|
1221
535
|
displayReadinessSummary(id);
|