@lumenflow/cli 2.3.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/__tests__/init-config-lanes.test.js +131 -0
  2. package/dist/__tests__/init-docs-structure.test.js +119 -0
  3. package/dist/__tests__/init-lane-inference.test.js +125 -0
  4. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  5. package/dist/__tests__/init-quick-ref.test.js +145 -0
  6. package/dist/__tests__/init-scripts.test.js +96 -0
  7. package/dist/__tests__/init-template-portability.test.js +97 -0
  8. package/dist/__tests__/init.test.js +199 -3
  9. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  10. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  11. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  12. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  13. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  14. package/dist/__tests__/plan-create.test.js +126 -0
  15. package/dist/__tests__/plan-edit.test.js +157 -0
  16. package/dist/__tests__/plan-link.test.js +239 -0
  17. package/dist/__tests__/plan-promote.test.js +181 -0
  18. package/dist/__tests__/wu-create-strict.test.js +118 -0
  19. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  20. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  21. package/dist/flow-bottlenecks.js +4 -2
  22. package/dist/flow-report.js +3 -2
  23. package/dist/gates.js +202 -2
  24. package/dist/init.js +720 -40
  25. package/dist/initiative-add-wu.js +112 -16
  26. package/dist/initiative-plan.js +3 -2
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/mem-context.js +0 -0
  29. package/dist/metrics-snapshot.js +3 -2
  30. package/dist/onboarding-smoke-test.js +400 -0
  31. package/dist/plan-create.js +199 -0
  32. package/dist/plan-edit.js +235 -0
  33. package/dist/plan-link.js +233 -0
  34. package/dist/plan-promote.js +231 -0
  35. package/dist/rotate-progress.js +8 -5
  36. package/dist/spawn-list.js +4 -3
  37. package/dist/state-bootstrap.js +6 -4
  38. package/dist/state-doctor-fix.js +5 -4
  39. package/dist/state-doctor.js +32 -2
  40. package/dist/trace-gen.js +6 -3
  41. package/dist/wu-block.js +16 -5
  42. package/dist/wu-claim.js +15 -9
  43. package/dist/wu-create.js +50 -2
  44. package/dist/wu-deps.js +3 -1
  45. package/dist/wu-done.js +14 -5
  46. package/dist/wu-edit.js +35 -0
  47. package/dist/wu-infer-lane.js +3 -1
  48. package/dist/wu-spawn.js +8 -0
  49. package/dist/wu-unblock.js +34 -2
  50. package/dist/wu-validate.js +25 -17
  51. package/package.json +12 -6
  52. package/templates/core/AGENTS.md.template +2 -2
  53. package/dist/__tests__/init-plan.test.js +0 -340
  54. package/dist/agent-issues-query.d.ts +0 -16
  55. package/dist/agent-log-issue.d.ts +0 -10
  56. package/dist/agent-session-end.d.ts +0 -10
  57. package/dist/agent-session.d.ts +0 -10
  58. package/dist/backlog-prune.d.ts +0 -84
  59. package/dist/cli-entry-point.d.ts +0 -8
  60. package/dist/deps-add.d.ts +0 -91
  61. package/dist/deps-remove.d.ts +0 -17
  62. package/dist/docs-sync.d.ts +0 -50
  63. package/dist/file-delete.d.ts +0 -84
  64. package/dist/file-edit.d.ts +0 -82
  65. package/dist/file-read.d.ts +0 -92
  66. package/dist/file-write.d.ts +0 -90
  67. package/dist/flow-bottlenecks.d.ts +0 -16
  68. package/dist/flow-report.d.ts +0 -16
  69. package/dist/gates.d.ts +0 -94
  70. package/dist/git-branch.d.ts +0 -65
  71. package/dist/git-diff.d.ts +0 -58
  72. package/dist/git-log.d.ts +0 -69
  73. package/dist/git-status.d.ts +0 -58
  74. package/dist/guard-locked.d.ts +0 -62
  75. package/dist/guard-main-branch.d.ts +0 -50
  76. package/dist/guard-worktree-commit.d.ts +0 -59
  77. package/dist/index.d.ts +0 -10
  78. package/dist/init-plan.d.ts +0 -80
  79. package/dist/init-plan.js +0 -337
  80. package/dist/init.d.ts +0 -46
  81. package/dist/initiative-add-wu.d.ts +0 -22
  82. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  83. package/dist/initiative-create.d.ts +0 -28
  84. package/dist/initiative-edit.d.ts +0 -34
  85. package/dist/initiative-list.d.ts +0 -12
  86. package/dist/initiative-status.d.ts +0 -11
  87. package/dist/lumenflow-upgrade.d.ts +0 -103
  88. package/dist/mem-checkpoint.d.ts +0 -16
  89. package/dist/mem-cleanup.d.ts +0 -29
  90. package/dist/mem-create.d.ts +0 -17
  91. package/dist/mem-export.d.ts +0 -10
  92. package/dist/mem-inbox.d.ts +0 -35
  93. package/dist/mem-init.d.ts +0 -15
  94. package/dist/mem-ready.d.ts +0 -16
  95. package/dist/mem-signal.d.ts +0 -16
  96. package/dist/mem-start.d.ts +0 -16
  97. package/dist/mem-summarize.d.ts +0 -22
  98. package/dist/mem-triage.d.ts +0 -22
  99. package/dist/metrics-cli.d.ts +0 -90
  100. package/dist/metrics-snapshot.d.ts +0 -18
  101. package/dist/orchestrate-init-status.d.ts +0 -11
  102. package/dist/orchestrate-initiative.d.ts +0 -12
  103. package/dist/orchestrate-monitor.d.ts +0 -11
  104. package/dist/release.d.ts +0 -117
  105. package/dist/rotate-progress.d.ts +0 -48
  106. package/dist/session-coordinator.d.ts +0 -74
  107. package/dist/spawn-list.d.ts +0 -16
  108. package/dist/state-bootstrap.d.ts +0 -92
  109. package/dist/sync-templates.d.ts +0 -52
  110. package/dist/trace-gen.d.ts +0 -84
  111. package/dist/validate-agent-skills.d.ts +0 -50
  112. package/dist/validate-agent-sync.d.ts +0 -36
  113. package/dist/validate-backlog-sync.d.ts +0 -37
  114. package/dist/validate-skills-spec.d.ts +0 -40
  115. package/dist/validate.d.ts +0 -60
  116. package/dist/wu-block.d.ts +0 -16
  117. package/dist/wu-claim.d.ts +0 -74
  118. package/dist/wu-cleanup.d.ts +0 -35
  119. package/dist/wu-create.d.ts +0 -69
  120. package/dist/wu-delete.d.ts +0 -21
  121. package/dist/wu-deps.d.ts +0 -13
  122. package/dist/wu-done.d.ts +0 -225
  123. package/dist/wu-edit.d.ts +0 -63
  124. package/dist/wu-infer-lane.d.ts +0 -17
  125. package/dist/wu-preflight.d.ts +0 -47
  126. package/dist/wu-prune.d.ts +0 -16
  127. package/dist/wu-recover.d.ts +0 -37
  128. package/dist/wu-release.d.ts +0 -19
  129. package/dist/wu-repair.d.ts +0 -60
  130. package/dist/wu-spawn-completion.d.ts +0 -10
  131. package/dist/wu-spawn.d.ts +0 -192
  132. package/dist/wu-status.d.ts +0 -25
  133. package/dist/wu-unblock.d.ts +0 -16
  134. package/dist/wu-unlock-lane.d.ts +0 -19
  135. package/dist/wu-validate.d.ts +0 -16
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable security/detect-non-literal-fs-filename */
3
+ /* eslint-disable no-console -- CLI tool requires console output */
3
4
  /**
4
5
  * Initiative Add WU Command (WU-1389)
5
6
  *
@@ -30,13 +31,118 @@ import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_LOG_PREFIX, } from '@lumenflow
30
31
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
31
32
  import { PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
32
33
  import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
33
- import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
34
+ import { withMicroWorktree, isRetryExhaustionError as coreIsRetryExhaustionError, formatRetryExhaustionError as coreFormatRetryExhaustionError, } from '@lumenflow/core/dist/micro-worktree.js';
34
35
  import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
35
36
  import { readInitiative, writeInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
37
+ import { validateSingleWU } from '@lumenflow/core/dist/validators/wu-tasks.js';
36
38
  /** Log prefix for console output */
37
39
  const LOG_PREFIX = INIT_LOG_PREFIX.ADD_WU;
38
40
  /** Micro-worktree operation name */
39
41
  const OPERATION_NAME = 'initiative-add-wu';
42
+ /**
43
+ * Validate WU spec for linking to an initiative (WU-1330)
44
+ *
45
+ * Performs validation including:
46
+ * - Schema validation (required fields, formats) - BLOCKING
47
+ * - Placeholder detection ([PLACEHOLDER] markers) - BLOCKING
48
+ * - Minimum description length (50 chars) - BLOCKING
49
+ * - Acceptance criteria present - BLOCKING
50
+ * - Completeness warnings (notes, spec_refs) - NON-BLOCKING
51
+ *
52
+ * Uses non-strict mode: schema errors block, completeness warnings don't.
53
+ *
54
+ * @param {string} wuId - WU ID to validate
55
+ * @returns {WULinkValidationResult} Validation result with errors and warnings
56
+ */
57
+ export function validateWUForLinking(wuId) {
58
+ const wuPath = WU_PATHS.WU(wuId);
59
+ // Check if file exists first
60
+ if (!existsSync(wuPath)) {
61
+ return {
62
+ valid: false,
63
+ errors: [`WU file not found: ${wuPath}`],
64
+ warnings: [],
65
+ };
66
+ }
67
+ // Use the core validator with non-strict mode
68
+ // Schema errors (required fields, format, placeholders) -> block
69
+ // Completeness warnings (notes, spec_refs) -> don't block
70
+ const result = validateSingleWU(wuPath, { strict: false });
71
+ return {
72
+ valid: result.valid,
73
+ errors: result.errors,
74
+ warnings: result.warnings,
75
+ };
76
+ }
77
+ /**
78
+ * WU-1333/WU-1336: Check if an error is a retry exhaustion error
79
+ *
80
+ * Detects when micro-worktree push retries have been exhausted.
81
+ * Delegates to the shared helper from @lumenflow/core.
82
+ *
83
+ * @param {Error} error - Error to check
84
+ * @returns {boolean} True if this is a retry exhaustion error
85
+ */
86
+ export function isRetryExhaustionError(error) {
87
+ return coreIsRetryExhaustionError(error);
88
+ }
89
+ /**
90
+ * WU-1333/WU-1336: Format retry exhaustion error with actionable next steps
91
+ *
92
+ * When push retries are exhausted, provides clear guidance on how to proceed.
93
+ * Delegates to the shared helper from @lumenflow/core with command-specific options.
94
+ *
95
+ * @param {Error} error - The retry exhaustion error
96
+ * @param {string} wuId - WU ID being linked
97
+ * @param {string} initId - Initiative ID being linked to
98
+ * @returns {string} Formatted error message with next steps
99
+ */
100
+ export function formatRetryExhaustionError(error, wuId, initId) {
101
+ return coreFormatRetryExhaustionError(error, {
102
+ command: `pnpm initiative:add-wu --wu ${wuId} --initiative ${initId}`,
103
+ });
104
+ }
105
+ /**
106
+ * Format validation errors for display to user (WU-1330)
107
+ *
108
+ * Creates a human-readable error message with all validation issues.
109
+ *
110
+ * @param {string} wuId - WU ID that failed validation
111
+ * @param {string[]} errors - Array of error messages
112
+ * @returns {string} Formatted error message
113
+ */
114
+ export function formatValidationErrors(wuId, errors) {
115
+ const errorList = errors.map((e) => ` - ${e}`).join('\n');
116
+ return (`WU ${wuId} failed validation:\n\n${errorList}\n\n` +
117
+ `Fix the WU spec before linking to an initiative:\n` +
118
+ ` pnpm wu:edit --id ${wuId} ...\n\n` +
119
+ `Or validate the WU:\n` +
120
+ ` pnpm wu:validate --id ${wuId}`);
121
+ }
122
+ /**
123
+ * Check if WU exists and is valid for linking (WU-1330)
124
+ *
125
+ * Combines existence check with strict validation.
126
+ * Throws with aggregated errors if validation fails.
127
+ *
128
+ * @param {string} wuId - WU ID to check and validate
129
+ * @returns {object} WU document if validation passes
130
+ * @throws {Error} If WU doesn't exist or validation fails
131
+ */
132
+ export function checkWUExistsAndValidate(wuId) {
133
+ const wuPath = WU_PATHS.WU(wuId);
134
+ // Check existence
135
+ if (!existsSync(wuPath)) {
136
+ die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
137
+ }
138
+ // Validate WU
139
+ const validation = validateWUForLinking(wuId);
140
+ if (!validation.valid) {
141
+ die(formatValidationErrors(wuId, validation.errors));
142
+ }
143
+ // Return the document if validation passes
144
+ return readWU(wuPath, wuId);
145
+ }
40
146
  /**
41
147
  * Validate Initiative ID format
42
148
  * @param {string} id - Initiative ID to validate
@@ -56,18 +162,6 @@ function validateWuIdFormat(id) {
56
162
  die(`Invalid WU ID format: "${id}"\n\nExpected format: WU-<number> (e.g., WU-123)`);
57
163
  }
58
164
  }
59
- /**
60
- * Check if WU exists
61
- * @param {string} wuId - WU ID to check
62
- * @returns {object} WU document
63
- */
64
- function checkWUExists(wuId) {
65
- const wuPath = WU_PATHS.WU(wuId);
66
- if (!existsSync(wuPath)) {
67
- die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
68
- }
69
- return readWU(wuPath, wuId);
70
- }
71
165
  /**
72
166
  * Check if Initiative exists
73
167
  * @param {string} initId - Initiative ID to check
@@ -164,10 +258,12 @@ async function main() {
164
258
  const wuId = args.wu;
165
259
  const initId = args.initiative;
166
260
  console.log(`${LOG_PREFIX} Linking ${wuId} to ${initId}...`);
167
- // Pre-flight validation
261
+ // Pre-flight validation: ID formats
168
262
  validateInitIdFormat(initId);
169
263
  validateWuIdFormat(wuId);
170
- const wuDoc = checkWUExists(wuId);
264
+ // WU-1330: Validate WU spec before linking
265
+ // This ensures only valid, complete WUs can be linked to initiatives
266
+ const wuDoc = checkWUExistsAndValidate(wuId);
171
267
  const initDoc = checkInitiativeExists(initId);
172
268
  // Check for conflicting links
173
269
  checkConflictingLink(wuDoc, initId);
@@ -232,5 +328,5 @@ async function main() {
232
328
  // path but import.meta.url resolves to the real path - they never match
233
329
  import { runCLI } from './cli-entry-point.js';
234
330
  if (import.meta.main) {
235
- runCLI(main);
331
+ void runCLI(main);
236
332
  }
@@ -31,12 +31,13 @@ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
31
31
  import { readInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
32
32
  import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
33
33
  import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
34
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
34
35
  /** Log prefix for console output */
35
36
  export const LOG_PREFIX = CORE_LOG_PREFIX.INITIATIVE_PLAN;
36
37
  /** Micro-worktree operation name */
37
38
  const OPERATION_NAME = 'initiative-plan';
38
- /** Standard plans directory relative to repo root */
39
- const PLANS_DIR = 'docs/04-operations/plans';
39
+ /** Standard plans directory relative to repo root (WU-1301: uses config-based paths) */
40
+ const PLANS_DIR = WU_PATHS.PLANS_DIR();
40
41
  /** LumenFlow URI scheme for plan references */
41
42
  const PLAN_URI_SCHEME = 'lumenflow://plans/';
42
43
  /**
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable security/detect-non-literal-fs-filename */
3
+ /* eslint-disable no-console -- CLI tool requires console output */
4
+ /**
5
+ * Initiative Remove WU Command (WU-1328)
6
+ *
7
+ * Unlinks a WU from an initiative bidirectionally:
8
+ * 1. Removes `initiative: INIT-NNN` field from WU YAML
9
+ * 2. Removes WU ID from initiative `wus: []` array
10
+ *
11
+ * Uses micro-worktree isolation for atomic operations.
12
+ *
13
+ * Usage:
14
+ * pnpm initiative:remove-wu --initiative INIT-001 --wu WU-123
15
+ *
16
+ * Features:
17
+ * - Validates both WU and initiative exist before modifying
18
+ * - Idempotent: no error if link does not exist
19
+ * - Handles partial state gracefully (WU has initiative but not in initiative's wus list)
20
+ * - Atomic: both files updated in single commit
21
+ *
22
+ * Context: WU-1328 (initial implementation)
23
+ */
24
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
25
+ import { die } from '@lumenflow/core/dist/error-handler.js';
26
+ import { existsSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
29
+ import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
30
+ import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_LOG_PREFIX, } from '@lumenflow/initiatives/dist/initiative-constants.js';
31
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
32
+ import { PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
33
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
34
+ import { withMicroWorktree, isRetryExhaustionError as coreIsRetryExhaustionError, formatRetryExhaustionError as coreFormatRetryExhaustionError, } from '@lumenflow/core/dist/micro-worktree.js';
35
+ import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
36
+ import { readInitiative, writeInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
37
+ /** Log prefix for console output */
38
+ export const LOG_PREFIX = INIT_LOG_PREFIX.REMOVE_WU;
39
+ /** Micro-worktree operation name */
40
+ export const OPERATION_NAME = 'initiative-remove-wu';
41
+ /**
42
+ * WU-1333/WU-1336: Check if an error is a retry exhaustion error
43
+ *
44
+ * Detects when micro-worktree push retries have been exhausted.
45
+ * Delegates to the shared helper from @lumenflow/core.
46
+ *
47
+ * @param {Error} error - Error to check
48
+ * @returns {boolean} True if this is a retry exhaustion error
49
+ */
50
+ export function isRetryExhaustionError(error) {
51
+ return coreIsRetryExhaustionError(error);
52
+ }
53
+ /**
54
+ * WU-1333/WU-1336: Format retry exhaustion error with actionable next steps
55
+ *
56
+ * When push retries are exhausted, provides clear guidance on how to proceed.
57
+ * Delegates to the shared helper from @lumenflow/core with command-specific options.
58
+ *
59
+ * @param {Error} error - The retry exhaustion error
60
+ * @param {string} wuId - WU ID being unlinked
61
+ * @param {string} initId - Initiative ID being unlinked from
62
+ * @returns {string} Formatted error message with next steps
63
+ */
64
+ export function formatRetryExhaustionError(error, wuId, initId) {
65
+ return coreFormatRetryExhaustionError(error, {
66
+ command: `pnpm initiative:remove-wu --wu ${wuId} --initiative ${initId}`,
67
+ });
68
+ }
69
+ /**
70
+ * Validate Initiative ID format
71
+ * @param {string} id - Initiative ID to validate
72
+ */
73
+ export function validateInitIdFormat(id) {
74
+ if (!INIT_PATTERNS.INIT_ID.test(id)) {
75
+ die(`Invalid Initiative ID format: "${id}"\n\n` +
76
+ `Expected format: INIT-<number> or INIT-<NAME> (e.g., INIT-001, INIT-TOOLING)`);
77
+ }
78
+ }
79
+ /**
80
+ * Validate WU ID format
81
+ * @param {string} id - WU ID to validate
82
+ */
83
+ export function validateWuIdFormat(id) {
84
+ if (!PATTERNS.WU_ID.test(id)) {
85
+ die(`Invalid WU ID format: "${id}"\n\nExpected format: WU-<number> (e.g., WU-123)`);
86
+ }
87
+ }
88
+ /**
89
+ * Check if WU exists
90
+ * @param {string} wuId - WU ID to check
91
+ * @returns {object} WU document
92
+ */
93
+ export function checkWUExists(wuId) {
94
+ const wuPath = WU_PATHS.WU(wuId);
95
+ if (!existsSync(wuPath)) {
96
+ die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
97
+ }
98
+ return readWU(wuPath, wuId);
99
+ }
100
+ /**
101
+ * Check if Initiative exists
102
+ * @param {string} initId - Initiative ID to check
103
+ * @returns {object} Initiative document
104
+ */
105
+ export function checkInitiativeExists(initId) {
106
+ const initPath = INIT_PATHS.INITIATIVE(initId);
107
+ if (!existsSync(initPath)) {
108
+ die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
109
+ }
110
+ return readInitiative(initPath, initId);
111
+ }
112
+ /**
113
+ * Check if WU is currently linked to the initiative
114
+ * @param {object} wuDoc - WU document
115
+ * @param {object} initDoc - Initiative document
116
+ * @param {string} wuId - WU ID
117
+ * @param {string} initId - Initiative ID
118
+ * @returns {boolean} True if WU is linked to the initiative (both sides)
119
+ */
120
+ export function checkWUIsLinked(wuDoc, initDoc, wuId, initId) {
121
+ const wuHasInit = wuDoc.initiative === initId;
122
+ const initHasWu = Array.isArray(initDoc.wus) && initDoc.wus.includes(wuId);
123
+ return wuHasInit && initHasWu;
124
+ }
125
+ /**
126
+ * Update WU YAML in micro-worktree (remove initiative field)
127
+ * @param {string} worktreePath - Path to micro-worktree
128
+ * @param {string} wuId - WU ID
129
+ * @param {string} initId - Initiative ID to remove
130
+ * @returns {boolean} True if changes were made
131
+ */
132
+ export function updateWUInWorktree(worktreePath, wuId, initId) {
133
+ const wuRelPath = WU_PATHS.WU(wuId);
134
+ const wuAbsPath = join(worktreePath, wuRelPath);
135
+ const doc = readWU(wuAbsPath, wuId);
136
+ // Skip if no initiative field or different initiative (idempotent)
137
+ if (!doc.initiative || doc.initiative !== initId) {
138
+ return false;
139
+ }
140
+ // Remove initiative field
141
+ delete doc.initiative;
142
+ writeWU(wuAbsPath, doc);
143
+ console.log(`${LOG_PREFIX} Removed initiative: ${initId} from ${wuId}`);
144
+ return true;
145
+ }
146
+ /**
147
+ * Update Initiative YAML in micro-worktree (remove WU from wus list)
148
+ * @param {string} worktreePath - Path to micro-worktree
149
+ * @param {string} initId - Initiative ID
150
+ * @param {string} wuId - WU ID to remove
151
+ * @returns {boolean} True if changes were made
152
+ */
153
+ export function updateInitiativeInWorktree(worktreePath, initId, wuId) {
154
+ const initRelPath = INIT_PATHS.INITIATIVE(initId);
155
+ const initAbsPath = join(worktreePath, initRelPath);
156
+ const doc = readInitiative(initAbsPath, initId);
157
+ // Skip if no wus array or WU not in list (idempotent)
158
+ if (!Array.isArray(doc.wus) || !doc.wus.includes(wuId)) {
159
+ return false;
160
+ }
161
+ // Remove WU from list
162
+ doc.wus = doc.wus.filter((id) => id !== wuId);
163
+ writeInitiative(initAbsPath, doc);
164
+ console.log(`${LOG_PREFIX} Removed ${wuId} from ${initId} wus list`);
165
+ return true;
166
+ }
167
+ export async function main() {
168
+ const args = createWUParser({
169
+ name: 'initiative-remove-wu',
170
+ description: 'Unlink a WU from an initiative bidirectionally',
171
+ options: [WU_OPTIONS.initiative, WU_OPTIONS.wu],
172
+ required: ['initiative', 'wu'],
173
+ allowPositionalId: false,
174
+ });
175
+ // Normalize args
176
+ const wuId = args.wu;
177
+ const initId = args.initiative;
178
+ console.log(`${LOG_PREFIX} Unlinking ${wuId} from ${initId}...`);
179
+ // Pre-flight validation
180
+ validateInitIdFormat(initId);
181
+ validateWuIdFormat(wuId);
182
+ const wuDoc = checkWUExists(wuId);
183
+ const initDoc = checkInitiativeExists(initId);
184
+ // Check if link exists - if not, report success (idempotent)
185
+ const wuHasInit = wuDoc.initiative === initId;
186
+ const initHasWu = Array.isArray(initDoc.wus) && initDoc.wus.includes(wuId);
187
+ if (!wuHasInit && !initHasWu) {
188
+ console.log(`${LOG_PREFIX} Link does not exist (idempotent - no changes needed)`);
189
+ console.log(`\n${LOG_PREFIX} ${wuId} is not linked to ${initId}`);
190
+ return;
191
+ }
192
+ // Ensure on main branch
193
+ await ensureOnMain(getGitForCwd());
194
+ // Transaction: micro-worktree isolation
195
+ try {
196
+ await withMicroWorktree({
197
+ operation: OPERATION_NAME,
198
+ id: `${wuId}-${initId}`.toLowerCase(),
199
+ logPrefix: LOG_PREFIX,
200
+ pushOnly: true,
201
+ execute: async ({ worktreePath }) => {
202
+ const files = [];
203
+ // Update WU YAML (remove initiative field)
204
+ const wuChanged = updateWUInWorktree(worktreePath, wuId, initId);
205
+ if (wuChanged) {
206
+ files.push(WU_PATHS.WU(wuId));
207
+ }
208
+ // Update Initiative YAML (remove WU from wus list)
209
+ const initChanged = updateInitiativeInWorktree(worktreePath, initId, wuId);
210
+ if (initChanged) {
211
+ files.push(INIT_PATHS.INITIATIVE(initId));
212
+ }
213
+ // If no changes, this is idempotent (race condition handling)
214
+ if (files.length === 0) {
215
+ console.log(`${LOG_PREFIX} No changes detected (concurrent unlink operation)`);
216
+ // Still need to return something for the commit
217
+ return {
218
+ commitMessage: INIT_COMMIT_FORMATS.UNLINK_WU(wuId, initId),
219
+ files: [WU_PATHS.WU(wuId), INIT_PATHS.INITIATIVE(initId)],
220
+ };
221
+ }
222
+ return {
223
+ commitMessage: INIT_COMMIT_FORMATS.UNLINK_WU(wuId, initId),
224
+ files,
225
+ };
226
+ },
227
+ });
228
+ console.log(`\n${LOG_PREFIX} Transaction complete!`);
229
+ console.log(`\nLink Removed:`);
230
+ console.log(` WU: ${wuId}`);
231
+ console.log(` Initiative: ${initId}`);
232
+ console.log(`\nNext steps:`);
233
+ console.log(` - View initiative status: pnpm initiative:status ${initId}`);
234
+ console.log(` - View WU: cat ${WU_PATHS.WU(wuId)}`);
235
+ }
236
+ catch (error) {
237
+ die(`Transaction failed: ${error.message}\n\n` +
238
+ `Micro-worktree cleanup was attempted automatically.\n` +
239
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
240
+ }
241
+ }
242
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
243
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
244
+ // path but import.meta.url resolves to the real path - they never match
245
+ import { runCLI } from './cli-entry-point.js';
246
+ if (import.meta.main) {
247
+ void runCLI(main);
248
+ }
File without changes
@@ -24,12 +24,13 @@ import { Command } from 'commander';
24
24
  import { captureMetricsSnapshot, } from '@lumenflow/metrics';
25
25
  import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
26
26
  import { die } from '@lumenflow/core/dist/error-handler.js';
27
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
27
28
  /** Log prefix for console output */
28
29
  const LOG_PREFIX = '[metrics:snapshot]';
29
30
  /** Default snapshot output path */
30
31
  const DEFAULT_OUTPUT = '.lumenflow/snapshots/metrics-latest.json';
31
- /** WU directory relative to repo root */
32
- const WU_DIR = 'docs/04-operations/tasks/wu';
32
+ /** WU directory relative to repo root (WU-1301: uses config-based paths) */
33
+ const WU_DIR = WU_PATHS.WU_DIR();
33
34
  /** Skip-gates audit file path */
34
35
  const SKIP_GATES_PATH = '.lumenflow/skip-gates-audit.ndjson';
35
36
  /** Snapshot type options */