@lumenflow/cli 2.18.3 → 2.20.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 +44 -42
- package/dist/agent-session.js +1 -1
- package/dist/agent-session.js.map +1 -1
- package/dist/commands/integrate.js +1 -0
- package/dist/commands/integrate.js.map +1 -1
- package/dist/commands.js +1 -0
- package/dist/commands.js.map +1 -1
- package/dist/delegation-list.js +140 -0
- package/dist/delegation-list.js.map +1 -0
- package/dist/docs-sync.js +1 -0
- package/dist/docs-sync.js.map +1 -1
- package/dist/doctor.js +36 -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 +6 -5
- 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 +137 -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-initiative.js +2 -2
- package/dist/orchestrate-initiative.js.map +1 -1
- package/dist/orchestrate-monitor.js +38 -38
- package/dist/orchestrate-monitor.js.map +1 -1
- package/dist/plan-link.js +7 -14
- package/dist/plan-link.js.map +1 -1
- package/dist/public-manifest.js +19 -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/sync-templates.js +2 -1
- package/dist/sync-templates.js.map +1 -1
- 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 +96 -1394
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-code-path-coverage.js +81 -0
- package/dist/wu-code-path-coverage.js.map +1 -0
- package/dist/wu-create-content.js +256 -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 +124 -0
- package/dist/wu-create-validation.js.map +1 -0
- package/dist/wu-create.js +45 -442
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +151 -249
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +401 -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 +43 -759
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-prep.js +43 -127
- 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-sandbox.js +253 -0
- package/dist/wu-sandbox.js.map +1 -0
- package/dist/wu-spawn-prompt-builders.js +1124 -0
- package/dist/wu-spawn-prompt-builders.js.map +1 -0
- package/dist/wu-spawn-strategy-resolver.js +319 -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/dist/wu-status.js +4 -0
- package/dist/wu-status.js.map +1 -1
- package/dist/wu-validate.js +1 -1
- package/dist/wu-validate.js.map +1 -1
- package/package.json +15 -11
- package/templates/core/LUMENFLOW.md.template +29 -99
- package/templates/core/UPGRADING.md.template +2 -2
- 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/core/ai/onboarding/release-process.md.template +1 -1
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/wu-edit.js
CHANGED
|
@@ -24,130 +24,37 @@
|
|
|
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
|
-
|
|
63
|
-
import { validateCodePathsExistence, validateTestPathsExistence, } from '@lumenflow/core/wu-preflight-validators';
|
|
64
|
-
import { BRANCH_PR_EDIT_MODE, BLOCKED_EDIT_MODE, resolveInProgressEditMode, } from './wu-state-cloud.js';
|
|
48
|
+
import { validateLaneFormat } from '@lumenflow/core/lane-checker';
|
|
65
49
|
import { runCLI } from './cli-entry-point.js';
|
|
66
|
-
|
|
50
|
+
// WU-1650: Import from decomposed modules
|
|
51
|
+
import { validateDoneWUEdits, validateWUEditable, validateWorktreeExists, validateWorktreeClean, validateWorktreeBranch, normalizeReplaceCodePathsArgv, EDIT_MODE, } from './wu-edit-validators.js';
|
|
52
|
+
import { applyEdits, applyEditsInWorktree, getWuEditCommitFiles, regenerateBacklogFromState, normalizeWUDates, updateInitiativeWusArrays, displayReadinessSummary, } from './wu-edit-operations.js';
|
|
53
|
+
// WU-1650: Re-export for backwards compatibility
|
|
54
|
+
// All test files and external consumers import from wu-edit.ts
|
|
55
|
+
export { validateDoneWUEdits, validateExposureValue, normalizeReplaceCodePathsArgv, hasScopeRelevantBranchChanges, } from './wu-edit-validators.js';
|
|
56
|
+
export { applyExposureEdit, applyEdits, mergeStringField, getWuEditCommitFiles, } from './wu-edit-operations.js';
|
|
67
57
|
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
58
|
/**
|
|
152
59
|
* Custom options for wu-edit (not in shared WU_OPTIONS)
|
|
153
60
|
*/
|
|
@@ -261,122 +168,13 @@ const EDIT_OPTIONS = {
|
|
|
261
168
|
flags: '--replace-dependencies',
|
|
262
169
|
description: 'Replace existing dependencies instead of appending',
|
|
263
170
|
},
|
|
171
|
+
// WU-1683: Plan field (symmetric with initiative related_plan)
|
|
172
|
+
plan: {
|
|
173
|
+
name: 'plan',
|
|
174
|
+
flags: '--plan <uri>',
|
|
175
|
+
description: 'Plan file URI (lumenflow://plans/... or repo-relative path)',
|
|
176
|
+
},
|
|
264
177
|
};
|
|
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
178
|
/**
|
|
381
179
|
* Parse command line arguments
|
|
382
180
|
*/
|
|
@@ -419,9 +217,11 @@ function parseArgs() {
|
|
|
419
217
|
EDIT_OPTIONS.replaceBlockedBy,
|
|
420
218
|
EDIT_OPTIONS.addDep,
|
|
421
219
|
EDIT_OPTIONS.replaceDependencies,
|
|
220
|
+
// WU-1683: Plan field
|
|
221
|
+
EDIT_OPTIONS.plan,
|
|
422
222
|
// WU-1039: Add exposure for done WU metadata updates
|
|
423
223
|
WU_OPTIONS.exposure,
|
|
424
|
-
//
|
|
224
|
+
// Compatibility flag: reality checks now run in wu:prep/wu:done
|
|
425
225
|
WU_OPTIONS.noStrict,
|
|
426
226
|
],
|
|
427
227
|
required: ['id'],
|
|
@@ -432,260 +232,6 @@ function parseArgs() {
|
|
|
432
232
|
process.argv = originalArgv;
|
|
433
233
|
}
|
|
434
234
|
}
|
|
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
235
|
/**
|
|
690
236
|
* Ensure working tree is clean
|
|
691
237
|
*/
|
|
@@ -695,232 +241,6 @@ async function ensureCleanWorkingTree() {
|
|
|
695
241
|
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
242
|
}
|
|
697
243
|
}
|
|
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
244
|
/**
|
|
925
245
|
* Main entry point
|
|
926
246
|
*/
|
|
@@ -977,7 +297,9 @@ async function main() {
|
|
|
977
297
|
opts.blockedBy ||
|
|
978
298
|
opts.addDep ||
|
|
979
299
|
// WU-1039: Add exposure to hasEdits check
|
|
980
|
-
opts.exposure
|
|
300
|
+
opts.exposure ||
|
|
301
|
+
// WU-1683: Add plan to hasEdits check
|
|
302
|
+
opts.plan;
|
|
981
303
|
if (!hasEdits) {
|
|
982
304
|
die('No edits specified.\n\n' +
|
|
983
305
|
'Provide one of:\n' +
|
|
@@ -997,66 +319,40 @@ async function main() {
|
|
|
997
319
|
' --test-paths-e2e <path> Append e2e test paths (repeatable)\n' +
|
|
998
320
|
' --blocked-by <wuIds> Append WU IDs that block this WU (use --replace-blocked-by to overwrite)\n' +
|
|
999
321
|
' --add-dep <wuIds> Append WU IDs to dependencies (use --replace-dependencies to overwrite)\n' +
|
|
1000
|
-
' --exposure <type> Update exposure level (ui, api, backend-only, documentation)\n
|
|
322
|
+
' --exposure <type> Update exposure level (ui, api, backend-only, documentation)\n' +
|
|
323
|
+
' --plan <uri> Set plan file URI (lumenflow://plans/... or repo-relative)\n\n' +
|
|
1001
324
|
'Note: All array fields now append by default (WU-1225). Use --replace-* flags to overwrite.');
|
|
1002
325
|
}
|
|
1003
326
|
// Apply edits to get updated WU
|
|
1004
327
|
const updatedWU = applyEdits(originalWU, opts);
|
|
1005
328
|
// WU-2004: Normalize legacy schema fields before validation
|
|
1006
|
-
// Converts: summary
|
|
329
|
+
// Converts: summary->description, string risks->array, test_paths->tests, etc.
|
|
1007
330
|
const normalizedForValidation = normalizeWUSchema(updatedWU);
|
|
1008
331
|
// WU-1539: Validate WU structure after applying edits (fail-fast, allows placeholders)
|
|
1009
332
|
// WU-1750: Zod transforms normalize embedded newlines in arrays and strings
|
|
1010
333
|
const validationResult = validateReadyWU(normalizedForValidation);
|
|
1011
334
|
if (!validationResult.success) {
|
|
1012
335
|
const errors = validationResult.error.issues
|
|
1013
|
-
.map((issue) => `
|
|
336
|
+
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
|
|
1014
337
|
.join('\n');
|
|
1015
|
-
die(`${PREFIX}
|
|
338
|
+
die(`${PREFIX} WU YAML validation failed:\n\n${errors}\n\nFix the issues above and retry.`);
|
|
1016
339
|
}
|
|
1017
340
|
// WU-2253: Validate acceptance/code_paths consistency and invariants compliance
|
|
1018
341
|
// This blocks WU edits if acceptance references paths not in code_paths
|
|
1019
342
|
// or if code_paths conflicts with tools/invariants.yml
|
|
1020
343
|
const invariantsPath = join(process.cwd(), 'tools/invariants.yml');
|
|
1021
|
-
const lintResult = lintWUSpec(normalizedForValidation, { invariantsPath });
|
|
344
|
+
const lintResult = lintWUSpec(normalizedForValidation, { invariantsPath, phase: 'intent' });
|
|
1022
345
|
if (!lintResult.valid) {
|
|
1023
346
|
const formatted = formatLintErrors(lintResult.errors);
|
|
1024
|
-
die(`${PREFIX}
|
|
347
|
+
die(`${PREFIX} WU SPEC LINT FAILED:\n\n${formatted}\n` +
|
|
1025
348
|
`Fix the issues above before editing this WU.`);
|
|
1026
349
|
}
|
|
1027
350
|
// WU-1750: CRITICAL - Use transformed data for all subsequent operations
|
|
1028
351
|
// This ensures embedded newlines are normalized before YAML output
|
|
1029
352
|
const normalizedWU = validationResult.data;
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
if (!strict) {
|
|
1034
|
-
console.warn(`${PREFIX} WARNING: strict validation bypassed (--no-strict). Path existence checks skipped.`);
|
|
1035
|
-
}
|
|
1036
|
-
if (strict) {
|
|
1037
|
-
const rootDir = process.cwd();
|
|
1038
|
-
const strictErrors = [];
|
|
1039
|
-
// Validate code_paths exist
|
|
1040
|
-
if (normalizedWU.code_paths && normalizedWU.code_paths.length > 0) {
|
|
1041
|
-
const codePathsResult = validateCodePathsExistence(normalizedWU.code_paths, rootDir);
|
|
1042
|
-
if (!codePathsResult.valid) {
|
|
1043
|
-
strictErrors.push(...codePathsResult.errors);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
// Validate test_paths exist (unit, e2e - not manual)
|
|
1047
|
-
if (normalizedWU.tests) {
|
|
1048
|
-
const testPathsResult = validateTestPathsExistence(normalizedWU.tests, rootDir);
|
|
1049
|
-
if (!testPathsResult.valid) {
|
|
1050
|
-
strictErrors.push(...testPathsResult.errors);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (strictErrors.length > 0) {
|
|
1054
|
-
const errorList = strictErrors.map((e) => ` • ${e}`).join('\n');
|
|
1055
|
-
die(`${PREFIX} ❌ Strict validation failed:\n\n${errorList}\n\n` +
|
|
1056
|
-
`Options:\n` +
|
|
1057
|
-
` 1. Fix the paths in the WU spec to match actual files\n` +
|
|
1058
|
-
` 2. Use --no-strict to bypass path existence checks (not recommended)`);
|
|
1059
|
-
}
|
|
353
|
+
if (opts.noStrict) {
|
|
354
|
+
console.warn(`${PREFIX} WARNING: --no-strict is accepted for compatibility; ` +
|
|
355
|
+
`reality checks run in wu:prep/wu:done.`);
|
|
1060
356
|
}
|
|
1061
357
|
// Validate lane format if present (WU-923: block parent-only lanes with taxonomy)
|
|
1062
358
|
if (normalizedWU.lane) {
|
|
@@ -1088,19 +384,13 @@ async function main() {
|
|
|
1088
384
|
`Switch to the claimed branch and retry.`);
|
|
1089
385
|
}
|
|
1090
386
|
await ensureCleanWorkingTree();
|
|
1091
|
-
enforceInProgressCodePathCoverage({
|
|
1092
|
-
id,
|
|
1093
|
-
editOpts: opts,
|
|
1094
|
-
codePaths: normalizedWU.code_paths,
|
|
1095
|
-
cwd: process.cwd(),
|
|
1096
|
-
});
|
|
1097
387
|
await applyEditsInWorktree({
|
|
1098
388
|
worktreePath: process.cwd(),
|
|
1099
389
|
id,
|
|
1100
390
|
updatedWU: normalizedWU,
|
|
1101
391
|
});
|
|
1102
392
|
await getGitForCwd().push('origin', currentBranch);
|
|
1103
|
-
console.log(`${PREFIX}
|
|
393
|
+
console.log(`${PREFIX} Successfully edited ${id} on branch ${currentBranch}`);
|
|
1104
394
|
console.log(`${PREFIX} Changes committed and pushed to origin/${currentBranch}`);
|
|
1105
395
|
displayReadinessSummary(id);
|
|
1106
396
|
}
|
|
@@ -1146,19 +436,13 @@ async function main() {
|
|
|
1146
436
|
// Calculate expected branch and validate
|
|
1147
437
|
const expectedBranch = getLaneBranch(originalWU.lane, id);
|
|
1148
438
|
await validateWorktreeBranch(absoluteWorktreePath, expectedBranch, id);
|
|
1149
|
-
enforceInProgressCodePathCoverage({
|
|
1150
|
-
id,
|
|
1151
|
-
editOpts: opts,
|
|
1152
|
-
codePaths: normalizedWU.code_paths,
|
|
1153
|
-
cwd: absoluteWorktreePath,
|
|
1154
|
-
});
|
|
1155
439
|
// Apply edits in the worktree (WU-1750: use normalized data)
|
|
1156
440
|
await applyEditsInWorktree({
|
|
1157
441
|
worktreePath: absoluteWorktreePath,
|
|
1158
442
|
id,
|
|
1159
443
|
updatedWU: normalizedWU,
|
|
1160
444
|
});
|
|
1161
|
-
console.log(`${PREFIX}
|
|
445
|
+
console.log(`${PREFIX} Successfully edited ${id} in worktree`);
|
|
1162
446
|
console.log(`${PREFIX} Changes committed to lane branch`);
|
|
1163
447
|
// WU-1620: Display readiness summary
|
|
1164
448
|
displayReadinessSummary(id);
|
|
@@ -1190,7 +474,7 @@ async function main() {
|
|
|
1190
474
|
// Emergency fix Session 2: Use centralized stringifyYAML helper
|
|
1191
475
|
const yamlContent = stringifyYAML(normalizedWU);
|
|
1192
476
|
writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
1193
|
-
console.log(`${PREFIX}
|
|
477
|
+
console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
|
|
1194
478
|
// WU-1929: Handle bidirectional initiative updates
|
|
1195
479
|
if (initiativeChanged) {
|
|
1196
480
|
const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
|
|
@@ -1199,7 +483,7 @@ async function main() {
|
|
|
1199
483
|
// WU-1594: Keep backlog projection synchronized with WU lane/spec edits.
|
|
1200
484
|
const backlogPath = join(worktreePath, WU_PATHS.BACKLOG());
|
|
1201
485
|
await regenerateBacklogFromState(backlogPath);
|
|
1202
|
-
console.log(`${PREFIX}
|
|
486
|
+
console.log(`${PREFIX} Regenerated backlog.md in micro-worktree`);
|
|
1203
487
|
return {
|
|
1204
488
|
commitMessage: COMMIT_FORMATS.EDIT(id),
|
|
1205
489
|
files: getWuEditCommitFiles(id, extraFiles),
|
|
@@ -1215,7 +499,7 @@ async function main() {
|
|
|
1215
499
|
process.env.LUMENFLOW_WU_TOOL = previousWuTool;
|
|
1216
500
|
}
|
|
1217
501
|
}
|
|
1218
|
-
console.log(`${PREFIX}
|
|
502
|
+
console.log(`${PREFIX} Successfully edited ${id}`);
|
|
1219
503
|
console.log(`${PREFIX} Changes pushed to origin/main`);
|
|
1220
504
|
// WU-1620: Display readiness summary
|
|
1221
505
|
displayReadinessSummary(id);
|