@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.
Files changed (104) hide show
  1. package/README.md +42 -41
  2. package/dist/delegation-list.js +140 -0
  3. package/dist/delegation-list.js.map +1 -0
  4. package/dist/doctor.js +35 -99
  5. package/dist/doctor.js.map +1 -1
  6. package/dist/gates-plan-resolvers.js +150 -0
  7. package/dist/gates-plan-resolvers.js.map +1 -0
  8. package/dist/gates-runners.js +533 -0
  9. package/dist/gates-runners.js.map +1 -0
  10. package/dist/gates-types.js +3 -0
  11. package/dist/gates-types.js.map +1 -1
  12. package/dist/gates-utils.js +316 -0
  13. package/dist/gates-utils.js.map +1 -0
  14. package/dist/gates.js +44 -1016
  15. package/dist/gates.js.map +1 -1
  16. package/dist/hooks/enforcement-generator.js +16 -880
  17. package/dist/hooks/enforcement-generator.js.map +1 -1
  18. package/dist/hooks/enforcement-sync.js +1 -4
  19. package/dist/hooks/enforcement-sync.js.map +1 -1
  20. package/dist/hooks/generators/auto-checkpoint.js +123 -0
  21. package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
  22. package/dist/hooks/generators/enforce-worktree.js +188 -0
  23. package/dist/hooks/generators/enforce-worktree.js.map +1 -0
  24. package/dist/hooks/generators/index.js +16 -0
  25. package/dist/hooks/generators/index.js.map +1 -0
  26. package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
  27. package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
  28. package/dist/hooks/generators/require-wu.js +115 -0
  29. package/dist/hooks/generators/require-wu.js.map +1 -0
  30. package/dist/hooks/generators/session-start-recovery.js +101 -0
  31. package/dist/hooks/generators/session-start-recovery.js.map +1 -0
  32. package/dist/hooks/generators/signal-utils.js +52 -0
  33. package/dist/hooks/generators/signal-utils.js.map +1 -0
  34. package/dist/hooks/generators/warn-incomplete.js +65 -0
  35. package/dist/hooks/generators/warn-incomplete.js.map +1 -0
  36. package/dist/init-detection.js +228 -0
  37. package/dist/init-detection.js.map +1 -0
  38. package/dist/init-scaffolding.js +146 -0
  39. package/dist/init-scaffolding.js.map +1 -0
  40. package/dist/init-templates.js +1928 -0
  41. package/dist/init-templates.js.map +1 -0
  42. package/dist/init.js +136 -2425
  43. package/dist/init.js.map +1 -1
  44. package/dist/initiative-edit.js +42 -11
  45. package/dist/initiative-edit.js.map +1 -1
  46. package/dist/initiative-remove-wu.js +0 -0
  47. package/dist/initiative-status.js +29 -2
  48. package/dist/initiative-status.js.map +1 -1
  49. package/dist/mem-context.js +22 -9
  50. package/dist/mem-context.js.map +1 -1
  51. package/dist/orchestrate-init-status.js +32 -1
  52. package/dist/orchestrate-init-status.js.map +1 -1
  53. package/dist/orchestrate-monitor.js +38 -38
  54. package/dist/orchestrate-monitor.js.map +1 -1
  55. package/dist/public-manifest.js +12 -5
  56. package/dist/public-manifest.js.map +1 -1
  57. package/dist/shared-validators.js +1 -0
  58. package/dist/shared-validators.js.map +1 -1
  59. package/dist/spawn-list.js +0 -0
  60. package/dist/wu-claim-branch.js +121 -0
  61. package/dist/wu-claim-branch.js.map +1 -0
  62. package/dist/wu-claim-output.js +83 -0
  63. package/dist/wu-claim-output.js.map +1 -0
  64. package/dist/wu-claim-resume-handler.js +85 -0
  65. package/dist/wu-claim-resume-handler.js.map +1 -0
  66. package/dist/wu-claim-state.js +572 -0
  67. package/dist/wu-claim-state.js.map +1 -0
  68. package/dist/wu-claim-validation.js +439 -0
  69. package/dist/wu-claim-validation.js.map +1 -0
  70. package/dist/wu-claim-worktree.js +221 -0
  71. package/dist/wu-claim-worktree.js.map +1 -0
  72. package/dist/wu-claim.js +54 -1402
  73. package/dist/wu-claim.js.map +1 -1
  74. package/dist/wu-create-content.js +254 -0
  75. package/dist/wu-create-content.js.map +1 -0
  76. package/dist/wu-create-readiness.js +57 -0
  77. package/dist/wu-create-readiness.js.map +1 -0
  78. package/dist/wu-create-validation.js +149 -0
  79. package/dist/wu-create-validation.js.map +1 -0
  80. package/dist/wu-create.js +39 -441
  81. package/dist/wu-create.js.map +1 -1
  82. package/dist/wu-done.js +144 -249
  83. package/dist/wu-done.js.map +1 -1
  84. package/dist/wu-edit-operations.js +432 -0
  85. package/dist/wu-edit-operations.js.map +1 -0
  86. package/dist/wu-edit-validators.js +280 -0
  87. package/dist/wu-edit-validators.js.map +1 -0
  88. package/dist/wu-edit.js +27 -713
  89. package/dist/wu-edit.js.map +1 -1
  90. package/dist/wu-prep.js +32 -2
  91. package/dist/wu-prep.js.map +1 -1
  92. package/dist/wu-repair.js +1 -1
  93. package/dist/wu-repair.js.map +1 -1
  94. package/dist/wu-spawn-prompt-builders.js +1123 -0
  95. package/dist/wu-spawn-prompt-builders.js.map +1 -0
  96. package/dist/wu-spawn-strategy-resolver.js +314 -0
  97. package/dist/wu-spawn-strategy-resolver.js.map +1 -0
  98. package/dist/wu-spawn.js +9 -1398
  99. package/dist/wu-spawn.js.map +1 -1
  100. package/package.json +10 -7
  101. package/templates/core/LUMENFLOW.md.template +29 -99
  102. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
  103. package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
  104. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/wu-claim.js CHANGED
@@ -13,26 +13,24 @@
13
13
  *
14
14
  * WU-2542: This script imports utilities from @lumenflow/core package.
15
15
  * Full migration to thin shim pending @lumenflow/core CLI export implementation.
16
+ *
17
+ * WU-1649: Decomposed into focused modules:
18
+ * - wu-claim-validation.ts: Pre-flight validation, schema, lane/spec checks
19
+ * - wu-claim-state.ts: State update helpers (WU YAML, backlog, status)
20
+ * - wu-claim-worktree.ts: Worktree mode claim workflow
21
+ * - wu-claim-branch.ts: Branch-only mode claim workflow
22
+ * - wu-claim-output.ts: Output formatting and display helpers
23
+ * - wu-claim-resume-handler.ts: Resume/handoff mode handler
24
+ * - wu-claim-mode.ts: Mode resolution (pre-existing)
25
+ * - wu-claim-cloud.ts: Cloud claim helpers (pre-existing)
16
26
  */
17
27
  // WU-2542: Import from @lumenflow/core to establish shim layer dependency
18
28
  // eslint-disable-next-line sonarjs/unused-import -- Validates @lumenflow/core package link
19
29
  import { VERSION as _LUMENFLOW_VERSION } from '@lumenflow/core';
20
- import { existsSync, readFileSync, rmSync } from 'node:fs';
21
- import { access, readFile, writeFile, mkdir } from 'node:fs/promises';
30
+ import { rmSync } from 'node:fs';
22
31
  import path from 'node:path';
23
32
  import { isOrphanWorktree } from '@lumenflow/core/orphan-detector';
24
- // WU-1352: Use centralized YAML functions from wu-yaml.ts
25
- import { parseYAML, stringifyYAML } from '@lumenflow/core/wu-yaml';
26
- import { assertTransition } from '@lumenflow/core/state-machine';
27
- import { checkLaneFree, validateLaneFormat, checkWipJustification, } from '@lumenflow/core/lane-checker';
28
- // WU-1603: Atomic lane locking to prevent TOCTOU race conditions
29
- import { acquireLaneLock, releaseLaneLock, checkLaneLock, forceRemoveStaleLock, } from '@lumenflow/core/lane-lock';
30
- // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
31
- // WU-1213: Using deprecated sync API - async validate() requires larger refactor (separate WU)
32
- import { validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/code-path-validator';
33
- // WU-1574: parseBacklogFrontmatter/getSectionHeadings removed - state store replaces backlog parsing
34
- import { detectConflicts } from '@lumenflow/core/code-paths-overlap';
35
- import { getGitForCwd, createGitForPath } from '@lumenflow/core/git-adapter';
33
+ import { getGitForCwd } from '@lumenflow/core/git-adapter';
36
34
  import { die, getErrorMessage } from '@lumenflow/core/error-handler';
37
35
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
38
36
  // WU-1491: Mode resolution for --cloud and flag combinations
@@ -41,83 +39,35 @@ import { resolveClaimMode } from './wu-claim-mode.js';
41
39
  import { shouldSkipBranchExistsCheck, resolveBranchClaimExecution } from './wu-claim-cloud.js';
42
40
  // WU-1495: Cloud auto-detection from config-driven env signals
43
41
  import { detectCloudMode, resolveEffectiveCloudActivation, CLOUD_ACTIVATION_SOURCE, } from '@lumenflow/core/cloud-detect';
44
- import { WU_PATHS, getStateStoreDirFromBacklog } from '@lumenflow/core/wu-paths';
45
- import { BRANCHES, REMOTES, WU_STATUS, CLAIMED_MODES, STATUS_SECTIONS, PATTERNS, toKebab, LOG_PREFIX, GIT_REFS, MICRO_WORKTREE_OPERATIONS, COMMIT_FORMATS, EMOJI, FILE_SYSTEM, STRING_LITERALS, LUMENFLOW_PATHS, resolveWUStatus, } from '@lumenflow/core/wu-constants';
46
- import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
42
+ import { WU_PATHS } from '@lumenflow/core/wu-paths';
43
+ import { BRANCHES, REMOTES, CLAIMED_MODES, PATTERNS, toKebab, LOG_PREFIX, EMOJI, } from '@lumenflow/core/wu-constants';
44
+ import { shouldSkipRemoteOperations } from '@lumenflow/core/micro-worktree';
47
45
  import { ensureOnMain, ensureMainUpToDate } from '@lumenflow/core/wu-helpers';
48
46
  import { emitWUFlowEvent } from '@lumenflow/core/telemetry';
49
- import { checkLaneForOrphanDoneWU, repairWUInconsistency, } from '@lumenflow/core/wu-consistency-checker';
50
- import { emitMandatoryAgentAdvisory } from '@lumenflow/core/orchestration-advisory-loader';
51
- import { validateWU, generateAutoApproval } from '@lumenflow/core/wu-schema';
52
47
  import { startSessionForWU } from '@lumenflow/agent/auto-session';
53
- // WU-1473: Surface unread signals on claim for agent awareness
54
- import { surfaceUnreadSignals } from './hooks/enforcement-generator.js';
55
48
  import { getConfig } from '@lumenflow/core/config';
56
- import { detectFixableIssues, applyFixes, autoFixWUYaml, formatIssues, } from '@lumenflow/core/wu-yaml-fixer';
57
- import { validateSpecCompleteness } from '@lumenflow/core/wu-done-validators';
58
- import { hasManualTests, isDocsOrProcessType } from '@lumenflow/core/wu-type-helpers';
49
+ import { acquireLaneLock, releaseLaneLock, checkLaneLock, forceRemoveStaleLock, } from '@lumenflow/core/lane-lock';
59
50
  import { getAssignedEmail } from '@lumenflow/core/wu-claim-helpers';
60
- import { symlinkNodeModules, symlinkNestedNodeModules } from '@lumenflow/core/worktree-symlink';
61
- // WU-1572: Import WUStateStore for event-sourced state tracking
62
- import { WUStateStore } from '@lumenflow/core/wu-state-store';
63
- import { SpawnRegistryStore } from '@lumenflow/core/spawn-registry-store';
64
- // WU-1574: Import backlog generator to replace BacklogManager
65
- import { generateBacklog, generateStatus } from '@lumenflow/core/backlog-generator';
66
- // WU-2411: Import resume helpers for agent handoff
67
- import { resumeClaimForHandoff, getWorktreeUncommittedChanges, formatUncommittedChanges, createHandoffCheckpoint, } from '@lumenflow/core/wu-claim-resume';
68
- // WU-1211: Import initiative validation for status auto-progression
69
- import { shouldProgressInitiativeStatus, findInitiative, writeInitiative, getInitiativeWUs, } from '@lumenflow/initiatives';
70
- // ensureOnMain() moved to wu-helpers.ts (WU-1256)
71
- /**
72
- * WU-1473: Surface unread coordination signals and display them.
73
- * Fail-open: any error is logged as a warning, never blocks claim.
74
- *
75
- * @param baseDir - Project base directory for memory layer
76
- */
77
- async function surfaceUnreadSignalsForDisplay(baseDir) {
78
- try {
79
- const result = await surfaceUnreadSignals(baseDir);
80
- if (result.count > 0) {
81
- const MAX_DISPLAY = 5;
82
- console.log(`\n${PREFIX} Unread coordination signals (${result.count}):`);
83
- for (const signal of result.signals.slice(0, MAX_DISPLAY)) {
84
- const timestamp = new Date(signal.created_at).toLocaleTimeString();
85
- const scope = signal.wu_id ? ` [${signal.wu_id}]` : '';
86
- console.log(` - [${timestamp}]${scope} ${signal.message}`);
87
- }
88
- if (result.count > MAX_DISPLAY) {
89
- console.log(` ... and ${result.count - MAX_DISPLAY} more`);
90
- }
91
- console.log(` Run 'pnpm mem:inbox' for full list`);
92
- }
93
- }
94
- catch (err) {
95
- // WU-1473 AC4: Fail-open - never block claim on memory errors
96
- console.warn(`${PREFIX} Warning: Could not surface unread signals: ${getErrorMessage(err)}`);
97
- }
98
- }
99
- async function ensureCleanOrClaimOnlyWhenNoAuto() {
100
- // Require staged claim edits only if running with --no-auto
101
- const status = await getGitForCwd().getStatus();
102
- if (!status)
103
- die('No staged changes detected. Stage backlog/status/WU YAML claim edits first or omit --no-auto.');
104
- const staged = status
105
- .split(STRING_LITERALS.NEWLINE)
106
- .filter(Boolean)
107
- .filter((l) => l.startsWith('A ') || l.startsWith('M ') || l.startsWith('R '));
108
- // WU-1311: Use config-based paths instead of hardcoded docs/04-operations paths
109
- const config = getConfig();
110
- const wuDirPattern = config.directories.wuDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
111
- // eslint-disable-next-line security/detect-non-literal-regexp -- config path escaped for regex; not user input
112
- const wuYamlRegex = new RegExp(`${wuDirPattern}/WU-\\d+\\.yaml`);
113
- const hasClaimFiles = staged.some((l) => l.includes(config.directories.statusPath) ||
114
- l.includes(config.directories.backlogPath) ||
115
- wuYamlRegex.test(l));
116
- if (!hasClaimFiles) {
117
- console.error(status);
118
- die('Stage claim-related files (status/backlog/WU YAML) before running with --no-auto.');
119
- }
120
- }
51
+ // WU-1649: Import from extracted modules
52
+ import { runPreflightValidations, handleCodePathOverlap, validateBranchOnlyMode, } from './wu-claim-validation.js';
53
+ import { readWUTitle, getStagedChanges, ensureCleanOrClaimOnlyWhenNoAuto, applyCanonicalClaimUpdate, rollbackCanonicalClaim, recordClaimPickupEvidence, shouldApplyCanonicalClaimUpdate as shouldApplyCanonicalClaimUpdateFn, } from './wu-claim-state.js';
54
+ import { claimWorktreeMode } from './wu-claim-worktree.js';
55
+ import { claimBranchOnlyMode } from './wu-claim-branch.js';
56
+ import { handleResumeMode } from './wu-claim-resume-handler.js';
57
+ // ============================================================================
58
+ // RE-EXPORTS: Preserve public API for existing test consumers
59
+ // ============================================================================
60
+ // From wu-claim-validation.ts
61
+ export { resolveClaimStatus, validateManualTestsForClaim } from './wu-claim-validation.js';
62
+ // From wu-claim-state.ts
63
+ export { shouldApplyCanonicalClaimUpdate, shouldPersistClaimMetadataOnBranch, resolveClaimBaselineRef, buildRollbackYamlDoc, hasClaimPickupEvidence, recordClaimPickupEvidence, getWorktreeCommitFiles, } from './wu-claim-state.js';
64
+ // From wu-claim-output.ts
65
+ export { formatProjectDefaults, printProjectDefaults, printLifecycleNudge, } from './wu-claim-output.js';
66
+ // From wu-claim-worktree.ts
67
+ export { applyFallbackSymlinks } from './wu-claim-worktree.js';
68
+ // ============================================================================
69
+ // Cloud activation (kept in orchestrator since it's only used in main())
70
+ // ============================================================================
121
71
  const PREFIX = LOG_PREFIX.CLAIM;
122
72
  /**
123
73
  * Resolve branch-aware cloud activation for wu:claim.
@@ -136,1270 +86,9 @@ export function resolveCloudActivationForClaim(input) {
136
86
  currentBranch: input.currentBranch,
137
87
  });
138
88
  }
139
- /**
140
- * WU-1508: Enforce tests.manual at claim time for non-doc/process WUs.
141
- * This is non-bypassable (independent of --allow-incomplete) to fail early.
142
- */
143
- export function validateManualTestsForClaim(doc, id) {
144
- if (isDocsOrProcessType(doc?.type)) {
145
- return { valid: true };
146
- }
147
- if (hasManualTests(doc?.tests)) {
148
- return { valid: true };
149
- }
150
- return {
151
- valid: false,
152
- error: `${id}: Missing required tests.manual for non-documentation WU.\n` +
153
- `Add at least one manual verification step under tests.manual before claiming.`,
154
- };
155
- }
156
- export function resolveClaimStatus(status) {
157
- return resolveWUStatus(status, WU_STATUS.READY);
158
- }
159
- /**
160
- * Decide whether wu:claim should update canonical state on origin/main.
161
- *
162
- * Cloud branch-pr claims run on platform-managed branches and should not mutate
163
- * canonical state on main during claim; they commit claim metadata on their own branch.
164
- */
165
- export function shouldApplyCanonicalClaimUpdate(input) {
166
- if (input.noPush) {
167
- return false;
168
- }
169
- return !(input.isCloud && input.claimedMode === CLAIMED_MODES.BRANCH_PR);
170
- }
171
- /**
172
- * Decide whether wu:claim should write claim metadata directly to the active branch.
173
- */
174
- export function shouldPersistClaimMetadataOnBranch(input) {
175
- return input.noPush === true || input.claimedMode === CLAIMED_MODES.BRANCH_PR;
176
- }
177
- /**
178
- * WU-1521: Build a rolled-back version of a WU YAML doc by stripping claim metadata.
179
- *
180
- * When wu:claim fails after pushing YAML changes to origin/main but before
181
- * worktree creation succeeds, this function produces a clean doc that can be
182
- * written back to reset the WU to 'ready' state, enabling a clean retry.
183
- *
184
- * Pure function: does not mutate the input doc.
185
- *
186
- * @param doc - The claimed WU YAML document to roll back
187
- * @returns A new document with status=ready and claim metadata removed
188
- */
189
- export function buildRollbackYamlDoc(doc) {
190
- // Shallow-copy to avoid mutating the original
191
- const rolled = { ...doc };
192
- // Reset status back to ready
193
- rolled.status = WU_STATUS.READY;
194
- // Remove claim-specific metadata fields
195
- delete rolled.claimed_mode;
196
- delete rolled.claimed_branch; // WU-1589: Clear claimed_branch on rollback
197
- delete rolled.claimed_at;
198
- delete rolled.worktree_path;
199
- delete rolled.baseline_main_sha;
200
- delete rolled.session_id;
201
- delete rolled.assigned_to;
202
- return rolled;
203
- }
204
- /**
205
- * Returns true when a spawn record includes claim-time pickup evidence.
206
- */
207
- export function hasClaimPickupEvidence(entry) {
208
- const pickedUpAt = typeof entry?.pickedUpAt === 'string' && entry.pickedUpAt.trim().length > 0
209
- ? entry.pickedUpAt
210
- : '';
211
- const pickedUpBy = typeof entry?.pickedUpBy === 'string' && entry.pickedUpBy.trim().length > 0
212
- ? entry.pickedUpBy
213
- : '';
214
- return pickedUpAt.length > 0 && pickedUpBy.length > 0;
215
- }
216
- /**
217
- * WU-1605: Record delegated pickup evidence at wu:claim time when a spawn/delegate
218
- * provenance record already exists for this target WU.
219
- */
220
- export async function recordClaimPickupEvidence(id, options = {}) {
221
- const baseDir = options.baseDir ?? process.cwd();
222
- const claimedBy = typeof options.claimedBy === 'string' && options.claimedBy.trim().length > 0
223
- ? options.claimedBy.trim()
224
- : 'unknown';
225
- const store = new SpawnRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
226
- await store.load();
227
- const spawnEntry = store.getByTarget(id);
228
- if (!spawnEntry) {
229
- return { matchedSpawn: false, recorded: false, alreadyRecorded: false };
230
- }
231
- if (hasClaimPickupEvidence(spawnEntry)) {
232
- return {
233
- matchedSpawn: true,
234
- recorded: false,
235
- alreadyRecorded: true,
236
- spawnId: spawnEntry.id,
237
- };
238
- }
239
- await store.recordPickup(spawnEntry.id, claimedBy);
240
- return {
241
- matchedSpawn: true,
242
- recorded: true,
243
- alreadyRecorded: false,
244
- spawnId: spawnEntry.id,
245
- };
246
- }
247
- /**
248
- * Pre-flight validation: Check WU file exists and is valid BEFORE any git operations
249
- * Prevents zombie worktrees when WU YAML is missing or malformed
250
- */
251
- function preflightValidateWU(WU_PATH, id) {
252
- // Check file exists
253
- if (!existsSync(WU_PATH)) {
254
- die(`WU file not found: ${WU_PATH}\n\n` +
255
- `Cannot claim a WU that doesn't exist.\n\n` +
256
- `Options:\n` +
257
- ` 1. Create the WU first: pnpm wu:create --id ${id} --lane <lane> --title "..."\n` +
258
- ` 2. Check if the WU ID is correct\n` +
259
- ` 3. Check if the WU file was moved or deleted`);
260
- }
261
- // Parse and validate YAML structure
262
- const text = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
263
- let doc;
264
- try {
265
- doc = parseYAML(text);
266
- }
267
- catch (e) {
268
- die(`Failed to parse WU YAML ${WU_PATH}\n\n` +
269
- `YAML parsing error: ${getErrorMessage(e)}\n\n` +
270
- `Fix the YAML syntax errors before claiming.`);
271
- }
272
- // Validate ID matches
273
- if (!doc || doc.id !== id) {
274
- die(`WU YAML id mismatch in ${WU_PATH}\n\n` +
275
- `Expected: ${id}\n` +
276
- `Found: ${doc?.id || 'missing'}\n\n` +
277
- `Fix the id field in the WU YAML before claiming.`);
278
- }
279
- // Validate state transition is allowed
280
- const currentStatus = resolveClaimStatus(doc.status);
281
- try {
282
- assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
283
- }
284
- catch (error) {
285
- die(`Cannot claim ${id} - invalid state transition\n\n` +
286
- `Current status: ${currentStatus}\n` +
287
- `Attempted transition: ${currentStatus} → in_progress\n\n` +
288
- `Reason: ${getErrorMessage(error)}`);
289
- }
290
- return doc;
291
- }
292
- /**
293
- * WU-1361: Validate YAML schema at claim time
294
- *
295
- * Validates WU YAML against Zod schema AFTER git pull.
296
- * Detects fixable issues BEFORE schema validation (so --fix can run even if schema fails).
297
- * Returns fixable issues for application in worktree (WU-1361 fix).
298
- *
299
- * @param {string} WU_PATH - Path to WU YAML file
300
- * @param {object} doc - Parsed WU YAML data
301
- * @param {object} args - CLI arguments
302
- * @param {boolean} args.fix - If true, issues will be fixed in worktree
303
- * @returns {Array} Array of fixable issues to apply in worktree
304
- */
305
- function validateYAMLSchema(WU_PATH, doc, args) {
306
- // WU-1361: Detect fixable issues BEFORE schema validation
307
- // This allows --fix to work even when schema would fail
308
- const fixableIssues = detectFixableIssues(doc);
309
- if (fixableIssues.length > 0) {
310
- if (args.fix) {
311
- // WU-1425: Apply fixes to in-memory doc so validation passes
312
- // Note: This does NOT modify the file on disk - only the in-memory object
313
- // The actual file fix happens when the doc is written to the worktree
314
- applyFixes(doc, fixableIssues);
315
- console.log(`${PREFIX} Detected ${fixableIssues.length} fixable YAML issue(s) (will fix in worktree):`);
316
- console.log(formatIssues(fixableIssues));
317
- }
318
- else {
319
- // Report issues and suggest --fix
320
- console.warn(`${PREFIX} Detected ${fixableIssues.length} fixable YAML issue(s):`);
321
- console.warn(formatIssues(fixableIssues));
322
- console.warn(`${PREFIX} Run with --fix to auto-repair these issues.`);
323
- // Continue - Zod validation will provide the detailed error
324
- }
325
- }
326
- // Now run Zod schema validation
327
- const schemaResult = validateWU(doc);
328
- if (!schemaResult.success) {
329
- const issueList = schemaResult.error.issues
330
- .map((i) => ` - ${i.path.join('.')}: ${i.message}`)
331
- .join(STRING_LITERALS.NEWLINE);
332
- const tip = fixableIssues.length > 0 ? 'Tip: Run with --fix to auto-repair common issues.\n' : '';
333
- die(`WU YAML schema validation failed for ${WU_PATH}:\n\n${issueList}\n\nFix these issues before claiming.\n${tip}`);
334
- }
335
- // WU-1361: Return fixable issues for application in worktree
336
- return args.fix ? fixableIssues : [];
337
- }
338
- // WU-1576: validateBacklogConsistency removed - repair now happens inside micro-worktree
339
- // See claimWorktreeMode() execute function for the new location
340
- async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null, gitAdapter = null, claimedBranch = null) {
341
- // Check file exists
342
- try {
343
- await access(WU_PATH);
344
- }
345
- catch {
346
- die(`WU file not found: ${WU_PATH}\n\n` +
347
- `Options:\n` +
348
- ` 1. Create the WU first: pnpm wu:create --id ${id} --lane "${lane}" --title "..."\n` +
349
- ` 2. Check if the WU ID is correct`);
350
- }
351
- // Read file
352
- let text;
353
- try {
354
- text = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
355
- }
356
- catch (e) {
357
- die(`Failed to read WU file: ${WU_PATH}\n\n` +
358
- `Error: ${getErrorMessage(e)}\n\n` +
359
- `Options:\n` +
360
- ` 1. Check file permissions: ls -la ${WU_PATH}\n` +
361
- ` 2. Ensure you have read access to the repository`);
362
- }
363
- let doc;
364
- try {
365
- doc = parseYAML(text);
366
- }
367
- catch (e) {
368
- die(`Failed to parse YAML ${WU_PATH}\n\n` +
369
- `Error: ${getErrorMessage(e)}\n\n` +
370
- `Options:\n` +
371
- ` 1. Validate YAML syntax: pnpm wu:validate --id ${id}\n` +
372
- ` 2. Fix YAML errors manually and retry`);
373
- }
374
- if (!doc || doc.id !== id) {
375
- die(`WU YAML id mismatch. Expected ${id}, found ${doc && doc.id}\n\n` +
376
- `Options:\n` +
377
- ` 1. Check the WU file has correct id field\n` +
378
- ` 2. Verify you're claiming the right WU`);
379
- }
380
- // Validate state transition before updating
381
- const currentStatus = resolveClaimStatus(doc.status);
382
- try {
383
- assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
384
- }
385
- catch (error) {
386
- die(`State transition validation failed: ${getErrorMessage(error)}`);
387
- }
388
- // Update status and lane (lane only if provided and different)
389
- doc.status = WU_STATUS.IN_PROGRESS;
390
- if (lane)
391
- doc.lane = lane;
392
- // Record claimed mode (worktree or branch-only)
393
- doc.claimed_mode = claimedMode;
394
- // WU-1590: Persist claimed_branch for branch-pr cloud agents so downstream commands
395
- // (wu:prep, wu:done, wu:cleanup) can resolve the actual branch via defaultBranchFrom()
396
- if (claimedBranch) {
397
- doc.claimed_branch = claimedBranch;
398
- }
399
- // WU-1226: Record worktree path to prevent resolution failures if lane field changes
400
- if (worktreePath) {
401
- doc.worktree_path = worktreePath;
402
- }
403
- const git = gitAdapter || getGitForCwd();
404
- // WU-1423: Record owner using validated email (no silent username fallback)
405
- // Fallback chain: git config user.email > GIT_AUTHOR_EMAIL > error
406
- // WU-1427: getAssignedEmail is now async to properly await gitAdapter.getConfigValue
407
- doc.assigned_to = await getAssignedEmail(git);
408
- // Record claim timestamp for duration tracking (WU-637)
409
- doc.claimed_at = new Date().toISOString();
410
- // WU-1382: Store baseline main SHA for parallel agent detection
411
- // wu:done will compare against this to detect if other WUs were merged during work
412
- doc.baseline_main_sha = await git.getCommitHash(GIT_REFS.ORIGIN_MAIN);
413
- // WU-1438: Store agent session ID for tracking
414
- if (sessionId) {
415
- doc.session_id = sessionId;
416
- }
417
- // WU-2080: Agent-first auto-approval
418
- // Agents auto-approve on claim. Human escalation only for detected triggers.
419
- const autoApproval = generateAutoApproval(doc, doc.assigned_to);
420
- doc.approved_by = autoApproval.approved_by;
421
- doc.approved_at = autoApproval.approved_at;
422
- doc.escalation_triggers = autoApproval.escalation_triggers;
423
- doc.requires_human_escalation = autoApproval.requires_human_escalation;
424
- // Log escalation triggers if any detected
425
- if (autoApproval.requires_human_escalation) {
426
- console.log(`[wu-claim] ⚠️ Escalation triggers detected: ${autoApproval.escalation_triggers.join(', ')}`);
427
- console.log(`[wu-claim] ℹ️ Human resolution required before wu:done can complete.`);
428
- }
429
- else {
430
- console.log(`[wu-claim] ✅ Agent auto-approved (no escalation triggers)`);
431
- }
432
- // WU-1352: Use centralized stringify for consistent output
433
- const out = stringifyYAML(doc);
434
- // Write file
435
- await writeFile(WU_PATH, out, { encoding: FILE_SYSTEM.UTF8 });
436
- // WU-1211: Return both title and initiative for status progression check
437
- return { title: doc.title || '', initiative: doc.initiative || null };
438
- }
439
- /**
440
- * WU-1211: Check and progress initiative status from draft/open to in_progress.
441
- *
442
- * Called when a WU with an initiative field is claimed. If this is the first
443
- * WU being claimed for the initiative, progress the initiative status.
444
- *
445
- * @param {string} worktreePath - Path to micro-worktree (or main)
446
- * @param {string} initiativeRef - Initiative ID or slug
447
- * @param {string} wuId - WU ID being claimed
448
- * @returns {Promise<{updated: boolean, initPath: string|null}>} Result
449
- */
450
- async function maybeProgressInitiativeStatus(worktreePath, initiativeRef, wuId) {
451
- try {
452
- // Find the initiative
453
- const initiative = findInitiative(initiativeRef);
454
- if (!initiative) {
455
- console.log(`${PREFIX} Initiative ${initiativeRef} not found (may be created later)`);
456
- return { updated: false, initPath: null };
457
- }
458
- // Get all WUs for this initiative to check if any are in_progress
459
- const wus = getInitiativeWUs(initiativeRef);
460
- // Include the WU we're currently claiming as in_progress
461
- const wusWithCurrent = wus.map((wu) => wu.id === wuId ? { ...wu, doc: { ...wu.doc, status: 'in_progress' } } : wu);
462
- const wuDocs = wusWithCurrent.map((wu) => wu.doc);
463
- // Check if initiative status should progress
464
- const progressCheck = shouldProgressInitiativeStatus(initiative.doc, wuDocs);
465
- if (!progressCheck.shouldProgress || !progressCheck.newStatus) {
466
- return { updated: false, initPath: null };
467
- }
468
- // Update initiative status in worktree
469
- const initRelativePath = initiative.path.replace(process.cwd() + '/', '');
470
- const initAbsPath = path.join(worktreePath, initRelativePath);
471
- // Read, update, write
472
- const initDoc = { ...initiative.doc, status: progressCheck.newStatus };
473
- writeInitiative(initAbsPath, initDoc);
474
- console.log(`${PREFIX} ✅ Initiative ${initiativeRef} status progressed: ${initiative.doc.status} → ${progressCheck.newStatus}`);
475
- return { updated: true, initPath: initRelativePath };
476
- }
477
- catch (error) {
478
- // Non-fatal: log warning and continue
479
- console.warn(`${PREFIX} ⚠️ Could not check initiative status progression: ${getErrorMessage(error)}`);
480
- return { updated: false, initPath: null };
481
- }
482
- }
483
- async function addOrReplaceInProgressStatus(statusPath, id, title) {
484
- // Check file exists
485
- try {
486
- await access(statusPath);
487
- }
488
- catch {
489
- die(`Missing ${statusPath}`);
490
- }
491
- const rel = `wu/${id}.yaml`;
492
- const bullet = `- [${id} — ${title}](${rel})`;
493
- // Read file
494
- const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
495
- const lines = content.split(STRING_LITERALS.NEWLINE);
496
- const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
497
- const startIdx = findHeader(STATUS_SECTIONS.IN_PROGRESS);
498
- if (startIdx === -1)
499
- die(`Could not find "${STATUS_SECTIONS.IN_PROGRESS}" section in status.md`);
500
- let endIdx = lines.slice(startIdx + 1).findIndex((l) => l.startsWith('## '));
501
- if (endIdx === -1)
502
- endIdx = lines.length - startIdx - 1;
503
- else
504
- endIdx = startIdx + 1 + endIdx;
505
- // Check if already present
506
- const section = lines.slice(startIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
507
- if (section.includes(rel) || section.includes(`[${id}`))
508
- return; // already listed
509
- // Remove "No items" marker if present
510
- for (let i = startIdx + 1; i < endIdx; i++) {
511
- if (lines[i] && lines[i].includes('No items currently in progress')) {
512
- lines.splice(i, 1);
513
- endIdx--;
514
- break;
515
- }
516
- }
517
- // Insert bullet right after header
518
- lines.splice(startIdx + 1, 0, '', bullet);
519
- // Write file
520
- await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
521
- encoding: FILE_SYSTEM.UTF8,
522
- });
523
- }
524
- async function removeFromReadyAndAddToInProgressBacklog(backlogPath, id, title, lane) {
525
- // WU-1574: Use WUStateStore as single source of truth, generate backlog.md from state
526
- // WU-1593: Use centralized path helper to correctly resolve state dir from backlog path
527
- const stateDir = getStateStoreDirFromBacklog(backlogPath);
528
- // Append claim event to state store
529
- const store = new WUStateStore(stateDir);
530
- await store.load();
531
- await store.claim(id, lane, title);
532
- console.log(`${PREFIX} Claim event appended to state store`);
533
- // Regenerate backlog.md from state store
534
- const backlogContent = await generateBacklog(store);
535
- await writeFile(backlogPath, backlogContent, { encoding: FILE_SYSTEM.UTF8 });
536
- console.log(`${PREFIX} backlog.md regenerated from state store`);
537
- // Regenerate status.md from state store
538
- const statusPath = path.join(path.dirname(backlogPath), 'status.md');
539
- const statusContent = await generateStatus(store);
540
- await writeFile(statusPath, statusContent, { encoding: FILE_SYSTEM.UTF8 });
541
- console.log(`${PREFIX} status.md regenerated from state store`);
542
- }
543
- /**
544
- * WU-1746: Append claim event without regenerating backlog.md/status.md
545
- * For worktree mode, we only need to record the claim event in the state store.
546
- * Generated files (backlog.md, status.md) cause merge conflicts when committed
547
- * to worktrees because they change on main as other WUs complete.
548
- *
549
- * @param {string} stateDir - Path to state store directory
550
- * @param {string} id - WU ID
551
- * @param {string} title - WU title
552
- * @param {string} lane - Lane name
553
- */
554
- async function appendClaimEventOnly(stateDir, id, title, lane) {
555
- const store = new WUStateStore(stateDir);
556
- await store.load();
557
- await store.claim(id, lane, title);
558
- console.log(`${PREFIX} Claim event appended to state store`);
559
- }
560
- /**
561
- * WU-1746: Get list of files to commit in worktree mode
562
- * Excludes backlog.md and status.md to prevent merge conflicts.
563
- * These generated files should only be updated on main during wu:done.
564
- *
565
- * @param {string} wuId - WU ID (e.g., 'WU-1746')
566
- * @returns {string[]} List of files to commit
567
- */
568
- export function getWorktreeCommitFiles(wuId) {
569
- // WU-1311: Use config-based paths instead of hardcoded docs/04-operations paths
570
- const config = getConfig();
571
- return [
572
- `${config.directories.wuDir}/${wuId}.yaml`,
573
- LUMENFLOW_PATHS.WU_EVENTS, // WU-1740: Event store is source of truth
574
- // WU-1746: Explicitly NOT including backlog.md and status.md
575
- // These generated files cause merge conflicts when main advances
576
- ];
577
- }
578
- function parseStagedChangeLine(line) {
579
- const parts = line.trim().split(/\s+/);
580
- const status = parts[0];
581
- if (!status)
582
- return null;
583
- if (status.startsWith('R') || status.startsWith('C')) {
584
- return { status, from: parts[1], filePath: parts[2] };
585
- }
586
- return { status, filePath: parts.slice(1).join(' ') };
587
- }
588
- async function getStagedChanges() {
589
- const diff = await getGitForCwd().raw(['diff', '--cached', '--name-status']);
590
- if (!diff.trim())
591
- return [];
592
- return diff
593
- .split(STRING_LITERALS.NEWLINE)
594
- .filter(Boolean)
595
- .map(parseStagedChangeLine)
596
- .filter(Boolean);
597
- }
598
- async function applyStagedChangesToMicroWorktree(worktreePath, stagedChanges) {
599
- for (const change of stagedChanges) {
600
- const filePath = change.filePath;
601
- if (!filePath)
602
- continue;
603
- const targetPath = path.join(worktreePath, filePath);
604
- if (change.status.startsWith('D')) {
605
- rmSync(targetPath, { recursive: true, force: true });
606
- continue;
607
- }
608
- const sourcePath = path.join(process.cwd(), filePath);
609
- const contents = await readFile(sourcePath, { encoding: FILE_SYSTEM.UTF8 });
610
- await mkdir(path.dirname(targetPath), { recursive: true });
611
- await writeFile(targetPath, contents, { encoding: FILE_SYSTEM.UTF8 });
612
- }
613
- }
614
- /**
615
- * Update canonical claim state on origin/main using push-only micro-worktree.
616
- * Ensures canonical state stays global while local main remains unchanged.
617
- */
618
- async function applyCanonicalClaimUpdate(ctx, sessionId) {
619
- const { args, id, laneK, worktree, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, fixableIssues, stagedChanges, currentBranchForCloud, // WU-1590: For persisting claimed_branch
620
- } = ctx;
621
- const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
622
- const worktreePathForYaml = claimedMode === CLAIMED_MODES.BRANCH_ONLY ? null : path.resolve(worktree);
623
- let updatedTitle = '';
624
- const filesToCommit = args.noAuto && stagedChanges.length > 0
625
- ? stagedChanges.map((change) => change.filePath).filter(Boolean)
626
- : [WU_PATHS.WU(id), WU_PATHS.STATUS(), WU_PATHS.BACKLOG(), LUMENFLOW_PATHS.WU_EVENTS];
627
- console.log(`${PREFIX} Updating canonical claim state (push-only)...`);
628
- await withMicroWorktree({
629
- operation: MICRO_WORKTREE_OPERATIONS.WU_CLAIM,
630
- id,
631
- logPrefix: PREFIX,
632
- pushOnly: true,
633
- execute: async ({ worktreePath }) => {
634
- const microWUPath = path.join(worktreePath, WU_PATH);
635
- const microStatusPath = path.join(worktreePath, STATUS_PATH);
636
- const microBacklogPath = path.join(worktreePath, BACKLOG_PATH);
637
- if (args.noAuto) {
638
- await applyStagedChangesToMicroWorktree(worktreePath, stagedChanges);
639
- }
640
- else {
641
- if (fixableIssues && fixableIssues.length > 0) {
642
- console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
643
- autoFixWUYaml(microWUPath);
644
- console.log(`${PREFIX} YAML fixes applied successfully`);
645
- }
646
- const microGit = createGitForPath(worktreePath);
647
- // WU-1211: updateWUYaml now returns {title, initiative}
648
- const updateResult = await updateWUYaml(microWUPath, id, args.lane, claimedMode, worktreePathForYaml, sessionId, microGit, currentBranchForCloud || null);
649
- updatedTitle = updateResult.title || updatedTitle;
650
- await addOrReplaceInProgressStatus(microStatusPath, id, updatedTitle);
651
- await removeFromReadyAndAddToInProgressBacklog(microBacklogPath, id, updatedTitle, args.lane);
652
- // WU-1211: Check and progress initiative status
653
- let initPath = null;
654
- if (updateResult.initiative) {
655
- const initProgress = await maybeProgressInitiativeStatus(worktreePath, updateResult.initiative, id);
656
- initPath = initProgress.initPath;
657
- }
658
- // Include initiative path in files to commit if updated
659
- const allFilesToCommit = initPath ? [...filesToCommit, initPath] : filesToCommit;
660
- return {
661
- commitMessage: commitMsg,
662
- files: allFilesToCommit,
663
- };
664
- }
665
- return {
666
- commitMessage: commitMsg,
667
- files: filesToCommit,
668
- };
669
- },
670
- });
671
- console.log(`${PREFIX} Canonical claim state updated on origin/main`);
672
- return updatedTitle;
673
- }
674
- /**
675
- * WU-1521: Rollback canonical claim state on origin/main after partial failure.
676
- *
677
- * When wu:claim pushes YAML changes to origin/main (via applyCanonicalClaimUpdate)
678
- * but then fails to create the worktree or branch, this function reverses the claim
679
- * by writing the WU YAML back to 'ready' status and emitting a 'release' event
680
- * to the state store. This ensures re-running wu:claim succeeds without wu:repair.
681
- *
682
- * Uses a push-only micro-worktree to atomically update origin/main.
683
- *
684
- * @param id - WU ID (e.g., 'WU-1521')
685
- * @param lane - Lane name for the release event
686
- * @param title - WU title for the release event
687
- */
688
- async function rollbackCanonicalClaim(id, _lane, _title) {
689
- console.log(`${PREFIX} Rolling back canonical claim for ${id}...`);
690
- try {
691
- await withMicroWorktree({
692
- operation: MICRO_WORKTREE_OPERATIONS.WU_CLAIM,
693
- id,
694
- logPrefix: PREFIX,
695
- pushOnly: true,
696
- execute: async ({ worktreePath }) => {
697
- const microWUPath = path.join(worktreePath, WU_PATHS.WU(id));
698
- // Read the current (claimed) YAML from the micro-worktree
699
- const text = await readFile(microWUPath, {
700
- encoding: FILE_SYSTEM.UTF8,
701
- });
702
- const doc = parseYAML(text);
703
- // Build the rolled-back doc and write it
704
- const rolledBackDoc = buildRollbackYamlDoc(doc);
705
- const out = stringifyYAML(rolledBackDoc);
706
- await writeFile(microWUPath, out, {
707
- encoding: FILE_SYSTEM.UTF8,
708
- });
709
- // Emit a release event to the state store so the claim event is reversed
710
- const microBacklogPath = path.join(worktreePath, WU_PATHS.BACKLOG());
711
- const stateDir = getStateStoreDirFromBacklog(microBacklogPath);
712
- const store = new WUStateStore(stateDir);
713
- await store.load();
714
- await store.release(id, `Rollback: wu:claim failed after canonical update`);
715
- // Regenerate backlog.md and status.md from the corrected state
716
- const backlogContent = await generateBacklog(store);
717
- await writeFile(microBacklogPath, backlogContent, {
718
- encoding: FILE_SYSTEM.UTF8,
719
- });
720
- const microStatusPath = path.join(worktreePath, WU_PATHS.STATUS());
721
- const statusContent = await generateStatus(store);
722
- await writeFile(microStatusPath, statusContent, {
723
- encoding: FILE_SYSTEM.UTF8,
724
- });
725
- return {
726
- commitMessage: `wu(${id.toLowerCase()}): rollback claim after partial failure`,
727
- files: [
728
- WU_PATHS.WU(id),
729
- WU_PATHS.STATUS(),
730
- WU_PATHS.BACKLOG(),
731
- LUMENFLOW_PATHS.WU_EVENTS,
732
- ],
733
- };
734
- },
735
- });
736
- console.log(`${PREFIX} Canonical claim rolled back for ${id}`);
737
- }
738
- catch (rollbackErr) {
739
- // Rollback failure should not mask the original error.
740
- // Log the rollback failure but let the original error propagate.
741
- console.error(`${PREFIX} WARNING: Failed to rollback canonical claim for ${id}: ${rollbackErr.message}`);
742
- console.error(`${PREFIX} Manual recovery required: pnpm wu:repair --id ${id} --claim`);
743
- }
744
- }
745
- async function readWUTitle(id) {
746
- const p = WU_PATHS.WU(id);
747
- // Check file exists
748
- try {
749
- await access(p);
750
- }
751
- catch {
752
- return null;
753
- }
754
- // Read file
755
- const text = await readFile(p, { encoding: FILE_SYSTEM.UTF8 });
756
- // Match title field - use RegExp.exec for sonarjs/prefer-regexp-exec compliance
757
- // Regex is safe: runs on trusted WU YAML files with bounded input
758
- const titlePattern = /^title:\s*"?([^"\n]+)"?$/m;
759
- const m = titlePattern.exec(text);
760
- return m ? m[1] : null;
761
- }
762
- // emitWUFlowEvent() moved to telemetry.ts as emitWUFlowEvent() (WU-1256)
763
- /**
764
- * Check if there's already a Branch-Only WU in progress
765
- * Branch-Only mode doesn't support parallel WUs (only one WU at a time in main checkout)
766
- * @param {string} statusPath - Path to status.md
767
- * @param {string} currentWU - Current WU ID being claimed
768
- * @returns {Promise<{hasBranchOnly: boolean, existingWU: string|null}>}
769
- */
770
- async function checkExistingBranchOnlyWU(statusPath, currentWU) {
771
- // Check file exists
772
- try {
773
- await access(statusPath);
774
- }
775
- catch {
776
- return { hasBranchOnly: false, existingWU: null };
777
- }
778
- // Read file
779
- const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
780
- const lines = content.split(STRING_LITERALS.NEWLINE);
781
- // Find "In Progress" section
782
- const startIdx = lines.findIndex((l) => l.trim().toLowerCase() === '## in progress');
783
- if (startIdx === -1)
784
- return { hasBranchOnly: false, existingWU: null };
785
- let endIdx = lines.slice(startIdx + 1).findIndex((l) => l.startsWith('## '));
786
- if (endIdx === -1)
787
- endIdx = lines.length - startIdx - 1;
788
- else
789
- endIdx = startIdx + 1 + endIdx;
790
- // Extract WU IDs from In Progress section
791
- // Use RegExp.exec for sonarjs/prefer-regexp-exec compliance
792
- const wuPattern = /\[?(WU-\d+)/i;
793
- const inProgressWUs = lines
794
- .slice(startIdx + 1, endIdx)
795
- .map((line) => {
796
- const match = wuPattern.exec(line);
797
- return match ? match[1].toUpperCase() : null;
798
- })
799
- .filter(Boolean)
800
- .filter((wuid) => wuid !== currentWU); // exclude the WU we're claiming
801
- // Check each in-progress WU for claimed_mode: branch-only
802
- for (const wuid of inProgressWUs) {
803
- const wuPath = WU_PATHS.WU(wuid);
804
- // Check file exists
805
- try {
806
- await access(wuPath);
807
- }
808
- catch {
809
- continue; // File doesn't exist, skip
810
- }
811
- try {
812
- // Read file
813
- const text = await readFile(wuPath, { encoding: FILE_SYSTEM.UTF8 });
814
- const doc = parseYAML(text);
815
- if (doc && doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY) {
816
- return { hasBranchOnly: true, existingWU: wuid };
817
- }
818
- }
819
- catch {
820
- // ignore parse errors
821
- }
822
- }
823
- return { hasBranchOnly: false, existingWU: null };
824
- }
825
- /**
826
- * Handle orphan WU check and auto-repair (WU-1276)
827
- * WU-1426: Commits repair changes to avoid dirty working tree blocking claim
828
- * WU-1437: Use pushOnly micro-worktree to keep local main pristine
829
- */
830
- async function handleOrphanCheck(lane, id) {
831
- const orphanCheck = await checkLaneForOrphanDoneWU(lane, id);
832
- if (orphanCheck.valid)
833
- return;
834
- // Try auto-repair for single orphan
835
- if (orphanCheck.orphans.length === 1) {
836
- const orphanId = orphanCheck.orphans[0];
837
- console.log(`${PREFIX} Auto-repairing orphan: ${orphanId}`);
838
- // WU-1437: Use micro-worktree with pushOnly to keep main pristine
839
- await withMicroWorktree({
840
- operation: MICRO_WORKTREE_OPERATIONS.ORPHAN_REPAIR,
841
- id: orphanId,
842
- logPrefix: PREFIX,
843
- pushOnly: true,
844
- execute: async ({ worktreePath }) => {
845
- // Run repair inside micro-worktree using projectRoot option
846
- const repairResult = await repairWUInconsistency(orphanCheck.reports[0], {
847
- projectRoot: worktreePath,
848
- });
849
- if (repairResult.failed > 0) {
850
- throw new Error(`Lane ${lane} has orphan done WU: ${orphanId}\n` +
851
- `Auto-repair failed. Fix manually with: pnpm wu:repair --id ${orphanId}`);
852
- }
853
- if (repairResult.repaired === 0) {
854
- // Nothing to repair - return empty result
855
- return { commitMessage: null, files: [] };
856
- }
857
- // Return files for commit
858
- // WU-1740: Include wu-events.jsonl to persist state store events
859
- return {
860
- commitMessage: `chore(repair): auto-repair orphan ${orphanId.toLowerCase()}`,
861
- files: [
862
- WU_PATHS.BACKLOG(),
863
- WU_PATHS.STATUS(),
864
- WU_PATHS.STAMP(orphanId),
865
- LUMENFLOW_PATHS.WU_EVENTS,
866
- ],
867
- };
868
- },
869
- });
870
- console.log(`${PREFIX} Auto-repair successful`);
871
- return;
872
- }
873
- die(`Lane ${lane} has ${orphanCheck.orphans.length} orphan done WUs: ${orphanCheck.orphans.join(', ')}\n` +
874
- `Fix with: pnpm wu:repair --id <WU-ID> for each, or pnpm wu:repair --all`);
875
- }
876
- /**
877
- * Validate lane format with user-friendly error messages
878
- */
879
- function validateLaneFormatWithError(lane) {
880
- try {
881
- validateLaneFormat(lane);
882
- }
883
- catch (error) {
884
- die(`Invalid lane format: ${getErrorMessage(error)}\n\n` +
885
- `Valid formats:\n` +
886
- ` - Parent-only: "Operations", "Intelligence", "Experience", etc.\n` +
887
- ` - Sub-lane: "Operations: Tooling", "Intelligence: Prompts", etc.\n\n` +
888
- `Format rules:\n` +
889
- ` - Single colon with EXACTLY one space after (e.g., "Parent: Subdomain")\n` +
890
- ` - No spaces before colon\n` +
891
- ` - No multiple colons\n\n` +
892
- `See .lumenflow.config.yaml for valid parent lanes.`);
893
- }
894
- }
895
- /**
896
- * Handle lane occupancy check and enforce WIP limit policy
897
- *
898
- * WU-1016: Updated to support configurable WIP limits per lane.
899
- * The WIP limit is read from .lumenflow.config.yaml and defaults to 1.
900
- */
901
- function handleLaneOccupancy(laneCheck, lane, id, force) {
902
- if (laneCheck.free)
903
- return;
904
- if (laneCheck.error) {
905
- die(`Lane check failed: ${laneCheck.error}`);
906
- }
907
- if (!laneCheck.occupiedBy)
908
- return;
909
- // WU-1016: Include WIP limit info in messages
910
- const wipLimit = laneCheck.wipLimit ?? 1;
911
- const currentCount = laneCheck.currentCount ?? 0;
912
- const inProgressList = laneCheck.inProgressWUs?.join(', ') || laneCheck.occupiedBy;
913
- if (force) {
914
- console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}" has ${currentCount}/${wipLimit} WUs in progress`);
915
- console.warn(`${PREFIX} ⚠️ In progress: ${inProgressList}`);
916
- console.warn(`${PREFIX} ⚠️ Forcing WIP limit override. Risk of worktree collision!`);
917
- console.warn(`${PREFIX} ⚠️ Use only for P0 emergencies or manual recovery.`);
918
- return;
919
- }
920
- die(`Lane "${lane}" is at WIP limit (${currentCount}/${wipLimit}).\n\n` +
921
- `In progress: ${inProgressList}\n\n` +
922
- `LumenFlow enforces WIP limits per lane to maintain focus.\n` +
923
- `Current limit for "${lane}": ${wipLimit} (configure in .lumenflow.config.yaml)\n\n` +
924
- `Options:\n` +
925
- ` 1. Wait for a WU to complete or block\n` +
926
- ` 2. Choose a different lane\n` +
927
- ` 3. Increase wip_limit in .lumenflow.config.yaml\n` +
928
- ` 4. Use --force to override (P0 emergencies only)\n\n` +
929
- // WU-1311: Use config-based status path
930
- `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" ${getConfig().directories.statusPath}`);
931
- }
932
- /**
933
- * Handle code path overlap detection (WU-901)
934
- */
935
- function handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args) {
936
- if (!existsSync(WU_PATH))
937
- return;
938
- const wuContent = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
939
- const wuDoc = parseYAML(wuContent);
940
- const codePaths = wuDoc.code_paths || [];
941
- if (codePaths.length === 0)
942
- return;
943
- const overlapCheck = detectConflicts(STATUS_PATH, codePaths, id);
944
- emitWUFlowEvent({
945
- script: 'wu-claim',
946
- wu_id: id,
947
- step: 'overlap_check',
948
- conflicts_count: overlapCheck.conflicts.length,
949
- forced: args.forceOverlap || false,
950
- });
951
- if (overlapCheck.hasBlocker && !args.forceOverlap) {
952
- const conflictList = overlapCheck.conflicts
953
- .map((c) => {
954
- const displayedOverlaps = c.overlaps.slice(0, 3).join(', ');
955
- const remainingCount = c.overlaps.length - 3;
956
- const suffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
957
- return ` - ${c.wuid}: ${displayedOverlaps}${suffix}`;
958
- })
959
- .join(STRING_LITERALS.NEWLINE);
960
- // WU-1311: Use config-based status path in error message
961
- die(`Code path overlap detected with in-progress WUs:\n\n${conflictList}\n\n` +
962
- `Merge conflicts are guaranteed if both WUs proceed.\n\n` +
963
- `Options:\n` +
964
- ` 1. Wait for conflicting WU(s) to complete\n` +
965
- ` 2. Coordinate with agent working on conflicting WU\n` +
966
- ` 3. Use --force-overlap --reason "..." (emits telemetry for audit)\n\n` +
967
- `To check WU status: grep "${STATUS_SECTIONS.IN_PROGRESS}" ${getConfig().directories.statusPath}`);
968
- }
969
- if (args.forceOverlap) {
970
- if (!args.reason) {
971
- die('--force-overlap requires --reason "explanation" for audit trail');
972
- }
973
- emitWUFlowEvent({
974
- script: 'wu-claim',
975
- wu_id: id,
976
- event: 'overlap_forced',
977
- reason: args.reason,
978
- conflicts: overlapCheck.conflicts.map((c) => ({ wuid: c.wuid, files: c.overlaps })),
979
- });
980
- console.warn(`${PREFIX} ⚠️ WARNING: Overlap forced with reason: ${args.reason}`);
981
- }
982
- }
983
- /**
984
- * Validate branch-only mode can be used
985
- */
986
- async function validateBranchOnlyMode(STATUS_PATH, id) {
987
- const branchOnlyCheck = await checkExistingBranchOnlyWU(STATUS_PATH, id);
988
- if (branchOnlyCheck.hasBranchOnly) {
989
- die(`Branch-Only mode does not support parallel WUs.\n\n` +
990
- `Another Branch-Only WU is already in progress: ${branchOnlyCheck.existingWU}\n\n` +
991
- `Options:\n` +
992
- ` 1. Complete ${branchOnlyCheck.existingWU} first (pnpm wu:done --id ${branchOnlyCheck.existingWU})\n` +
993
- ` 2. Block ${branchOnlyCheck.existingWU} (pnpm wu:block --id ${branchOnlyCheck.existingWU} --reason "...")\n` +
994
- ` 3. Use Worktree mode instead (omit --branch-only flag)\n\n` +
995
- `Branch-Only mode works in the main checkout and cannot isolate parallel WUs.`);
996
- }
997
- // Ensure working directory is clean for Branch-Only mode
998
- const status = await getGitForCwd().getStatus();
999
- if (status) {
1000
- die(`Branch-Only mode requires a clean working directory.\n\n` +
1001
- `Uncommitted changes detected:\n${status}\n\n` +
1002
- `Options:\n` +
1003
- ` 1. Commit or stash your changes\n` +
1004
- ` 2. Use Worktree mode instead (omit --branch-only flag for isolated workspace)`);
1005
- }
1006
- }
1007
- /**
1008
- * Execute branch-only mode claim workflow
1009
- */
1010
- async function claimBranchOnlyMode(ctx) {
1011
- const { args, id, laneK, title, branch, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, shouldCreateBranch, currentBranch, sessionId, updatedTitle, currentBranchForCloud, // WU-1590: For persisting claimed_branch
1012
- } = ctx;
1013
- if (shouldCreateBranch) {
1014
- // Create branch and switch to it from origin/main (avoids local main mutation)
1015
- try {
1016
- await getGitForCwd().createBranch(branch, `${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
1017
- }
1018
- catch (error) {
1019
- die(`Canonical claim state may be updated, but branch creation failed.\n\n` +
1020
- `Error: ${getErrorMessage(error)}\n\n` +
1021
- `Recovery:\n` +
1022
- ` 1. Run: git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}\n` +
1023
- ` 2. Retry: pnpm wu:claim --id ${id} --lane "${args.lane}"\n` +
1024
- ` 3. If needed, delete local branch: git branch -D ${branch}`);
1025
- }
1026
- }
1027
- else if (currentBranch !== branch) {
1028
- die(`Cloud branch-pr claim must run on the active branch.\n\n` +
1029
- `Current branch: ${currentBranch}\n` +
1030
- `Resolved branch: ${branch}\n\n` +
1031
- `Switch to ${branch} and retry, or omit conflicting --branch flags.`);
1032
- }
1033
- let finalTitle = updatedTitle || title;
1034
- const msg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
1035
- const shouldPersistClaimMetadata = shouldPersistClaimMetadataOnBranch({
1036
- claimedMode,
1037
- noPush: Boolean(args.noPush),
1038
- });
1039
- if (shouldPersistClaimMetadata) {
1040
- if (args.noAuto) {
1041
- await ensureCleanOrClaimOnlyWhenNoAuto();
1042
- }
1043
- else {
1044
- // WU-1211: updateWUYaml now returns {title, initiative}
1045
- // WU-1590: Pass claimed_branch for branch-pr persistence
1046
- const updateResult = await updateWUYaml(WU_PATH, id, args.lane, claimedMode, null, sessionId, null, currentBranchForCloud || null);
1047
- finalTitle = updateResult.title || finalTitle;
1048
- await addOrReplaceInProgressStatus(STATUS_PATH, id, finalTitle);
1049
- await removeFromReadyAndAddToInProgressBacklog(BACKLOG_PATH, id, finalTitle, args.lane);
1050
- const filesToAdd = [WU_PATH, STATUS_PATH, BACKLOG_PATH];
1051
- // WU-1211: Progress initiative status if needed
1052
- if (updateResult.initiative) {
1053
- const initProgress = await maybeProgressInitiativeStatus(process.cwd(), updateResult.initiative, id);
1054
- if (initProgress.initPath) {
1055
- filesToAdd.push(initProgress.initPath);
1056
- }
1057
- }
1058
- await getGitForCwd().add(filesToAdd);
1059
- }
1060
- await getGitForCwd().commit(msg);
1061
- }
1062
- if (args.noPush) {
1063
- console.warn(`${PREFIX} Warning: --no-push enabled. Claim is local-only and NOT visible to other agents.`);
1064
- }
1065
- else {
1066
- await getGitForCwd().push(REMOTES.ORIGIN, branch, { setUpstream: true });
1067
- }
1068
- // Summary
1069
- console.log(`\n${PREFIX} Claim recorded in Branch-Only mode.`);
1070
- const wuDisplay = finalTitle ? `- WU: ${id} — ${finalTitle}` : `- WU: ${id}`;
1071
- console.log(wuDisplay);
1072
- console.log(`- Lane: ${args.lane}`);
1073
- console.log(`- Mode: Branch-Only (no worktree)`);
1074
- const refDisplay = args.noPush ? `- Commit: ${msg}` : `- Branch: ${branch}`;
1075
- console.log(refDisplay);
1076
- console.log('\n⚠️ LIMITATION: Branch-Only mode does not support parallel WUs (WIP=1 across ALL lanes)');
1077
- console.log('Next: work on this branch in the main checkout.');
1078
- // WU-1360: Print next-steps checklist to prevent common mistakes
1079
- console.log(`\n${PREFIX} Next steps:`);
1080
- console.log(` 1. Work on this branch in the main checkout`);
1081
- console.log(` 2. Implement changes per acceptance criteria`);
1082
- console.log(` 3. Run: pnpm gates`);
1083
- console.log(` 4. pnpm wu:done --id ${id}`);
1084
- console.log(`\n${PREFIX} Common mistakes to avoid:`);
1085
- console.log(` - Don't manually edit WU YAML status fields`);
1086
- console.log(` - Don't create PRs (trunk-based development)`);
1087
- // WU-1501: Hint for sub-agent execution context
1088
- console.log(`\n${PREFIX} For sub-agent execution:`);
1089
- console.log(` /wu-prompt ${id} (generates full context prompt)`);
1090
- // Emit mandatory agent advisory based on code_paths (WU-1324)
1091
- const wuContent = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
1092
- const wuDoc = parseYAML(wuContent);
1093
- const codePaths = wuDoc.code_paths || [];
1094
- emitMandatoryAgentAdvisory(codePaths, id);
1095
- // WU-1763: Print lifecycle nudge with tips for tool adoption
1096
- printLifecycleNudge(id);
1097
- // WU-1473: Surface unread coordination signals so agents see pending messages
1098
- // Fail-open: surfaceUnreadSignals never throws
1099
- await surfaceUnreadSignalsForDisplay(process.cwd());
1100
- }
1101
- /**
1102
- * WU-1213: Handle local-only claim metadata update (noPush mode).
1103
- * Extracted to reduce cognitive complexity of claimWorktreeMode.
1104
- *
1105
- * @returns {Promise<{finalTitle: string, initPathToCommit: string | null}>}
1106
- */
1107
- async function handleNoPushMetadataUpdate(ctx) {
1108
- const { args, id, worktree, worktreePath, WU_PATH, BACKLOG_PATH, claimedMode, fixableIssues, sessionId, title, updatedTitle, stagedChanges, } = ctx;
1109
- let finalTitle = updatedTitle || title;
1110
- let initPathToCommit = null;
1111
- if (args.noAuto) {
1112
- await applyStagedChangesToMicroWorktree(worktreePath, stagedChanges);
1113
- }
1114
- else {
1115
- const wtWUPath = path.join(worktreePath, WU_PATH);
1116
- const wtBacklogPath = path.join(worktreePath, BACKLOG_PATH);
1117
- if (fixableIssues && fixableIssues.length > 0) {
1118
- console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
1119
- autoFixWUYaml(wtWUPath);
1120
- console.log(`${PREFIX} YAML fixes applied successfully`);
1121
- }
1122
- const updateResult = await updateWUYaml(wtWUPath, id, args.lane, claimedMode, worktree, sessionId);
1123
- finalTitle = updateResult.title || finalTitle;
1124
- const wtStateDir = getStateStoreDirFromBacklog(wtBacklogPath);
1125
- await appendClaimEventOnly(wtStateDir, id, finalTitle, args.lane);
1126
- if (updateResult.initiative) {
1127
- const initProgress = await maybeProgressInitiativeStatus(worktreePath, updateResult.initiative, id);
1128
- initPathToCommit = initProgress.initPath;
1129
- }
1130
- }
1131
- return { finalTitle, initPathToCommit };
1132
- }
1133
- /**
1134
- * WU-1213: Setup worktree dependencies (symlink or full install).
1135
- * Extracted to reduce cognitive complexity of claimWorktreeMode.
1136
- */
1137
- async function setupWorktreeDependencies(worktreePath, originalCwd, skipSetup) {
1138
- // eslint-disable-next-line sonarjs/no-selector-parameter -- skipSetup mirrors CLI flag semantics
1139
- if (skipSetup) {
1140
- // WU-1443: Symlink-only mode for fast claims
1141
- const symlinkResult = symlinkNodeModules(worktreePath, console, originalCwd);
1142
- if (symlinkResult.created) {
1143
- console.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked (--skip-setup mode)`);
1144
- }
1145
- else if (symlinkResult.refused) {
1146
- console.warn(`${PREFIX} Warning: symlink refused: ${symlinkResult.reason}`);
1147
- console.warn(`${PREFIX} Run 'pnpm install' manually in the worktree`);
1148
- }
1149
- // WU-1579: Auto-symlink nested package node_modules for turbo typecheck
1150
- if (!symlinkResult.refused) {
1151
- const nestedResult = symlinkNestedNodeModules(worktreePath, originalCwd);
1152
- if (nestedResult.created > 0) {
1153
- console.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
1154
- }
1155
- }
1156
- }
1157
- else {
1158
- // WU-1023: Full setup mode (default) - run pnpm install with progress indicator
1159
- console.log(`${PREFIX} Installing worktree dependencies (this may take a moment)...`);
1160
- try {
1161
- const { execSync } = await import('node:child_process');
1162
- execSync('pnpm install --frozen-lockfile', {
1163
- cwd: worktreePath,
1164
- stdio: 'inherit',
1165
- timeout: 300000, // 5 minute timeout
1166
- });
1167
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Worktree dependencies installed`);
1168
- }
1169
- catch (installError) {
1170
- console.warn(`${PREFIX} Warning: pnpm install failed: ${installError.message}`);
1171
- console.warn(`${PREFIX} You may need to run 'pnpm install' manually in the worktree`);
1172
- console.log(`${PREFIX} Falling back to symlink approach...`);
1173
- applyFallbackSymlinks(worktreePath, originalCwd, console);
1174
- }
1175
- }
1176
- }
1177
- /**
1178
- * Execute worktree mode claim workflow
1179
- *
1180
- * WU-1741: Removed micro-worktree pattern that committed to main during claim.
1181
- * Branch existence (e.g. lane/operations/wu-1234) is the coordination lock.
1182
- * Metadata updates happen IN the work worktree, NOT on main.
1183
- *
1184
- * New flow:
1185
- * 1. Create work worktree+branch from main (branch = lock)
1186
- * 2. Update metadata (WU YAML, status.md, backlog.md) IN worktree
1187
- * 3. Commit metadata in worktree
1188
- * 4. Main only changes via wu:done (single merge point)
1189
- *
1190
- * Benefits:
1191
- * - Simpler mental model: main ONLY changes via wu:done
1192
- * - Branch existence is natural coordination (git prevents duplicates)
1193
- * - Less network traffic (no push during claim)
1194
- * - Cleaner rollback: delete worktree+branch = claim undone
1195
- */
1196
- async function claimWorktreeMode(ctx) {
1197
- const { args, id, laneK, title, branch, worktree, WU_PATH, updatedTitle } = ctx;
1198
- const originalCwd = process.cwd();
1199
- const worktreePath = path.resolve(worktree);
1200
- let finalTitle = updatedTitle || title;
1201
- const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
1202
- // WU-1741: Step 1 - Create work worktree+branch from main
1203
- console.log(`${PREFIX} Creating worktree (branch = coordination lock)...`);
1204
- const startPoint = args.noPush ? BRANCHES.MAIN : `${REMOTES.ORIGIN}/${BRANCHES.MAIN}`;
1205
- await getGitForCwd().worktreeAdd(worktree, branch, startPoint);
1206
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Worktree created at ${worktree}`);
1207
- if (!args.noPush) {
1208
- const wtGit = createGitForPath(worktreePath);
1209
- await wtGit.push(REMOTES.ORIGIN, branch, { setUpstream: true });
1210
- }
1211
- // Handle local-only claim metadata update (noPush mode)
1212
- if (args.noPush) {
1213
- const metadataResult = await handleNoPushMetadataUpdate({ ...ctx, worktreePath });
1214
- finalTitle = metadataResult.finalTitle;
1215
- // Commit metadata in worktree
1216
- console.log(`${PREFIX} Committing claim metadata in worktree...`);
1217
- const wtGit = createGitForPath(worktreePath);
1218
- const filesToCommit = getWorktreeCommitFiles(id);
1219
- if (metadataResult.initPathToCommit) {
1220
- filesToCommit.push(metadataResult.initPathToCommit);
1221
- }
1222
- await wtGit.add(filesToCommit);
1223
- await wtGit.commit(commitMsg);
1224
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Claim committed: ${commitMsg}`);
1225
- console.warn(`${PREFIX} Warning: --no-push enabled. Claim is local-only and NOT visible to other agents.`);
1226
- }
1227
- // WU-1023: Auto-setup worktree dependencies
1228
- await setupWorktreeDependencies(worktreePath, originalCwd, args.skipSetup);
1229
- console.log(`${PREFIX} Claim recorded in worktree`);
1230
- const worktreeWuDisplay = finalTitle ? `- WU: ${id} — ${finalTitle}` : `- WU: ${id}`;
1231
- console.log(worktreeWuDisplay);
1232
- console.log(`- Lane: ${args.lane}`);
1233
- console.log(`- Worktree: ${worktreePath}`);
1234
- console.log(`- Branch: ${branch}`);
1235
- console.log(`- Commit: ${commitMsg}`);
1236
- // Summary
1237
- console.log(`\n${PREFIX} Worktree created and claim committed.`);
1238
- console.log(`Next: cd ${worktree} and begin work.`);
1239
- // WU-1360: Print next-steps checklist to prevent common mistakes
1240
- console.log(`\n${PREFIX} Next steps:`);
1241
- console.log(` 1. cd ${worktree} (IMPORTANT: work here, not main)`);
1242
- console.log(` 2. Implement changes per acceptance criteria`);
1243
- console.log(` 3. Run: pnpm gates`);
1244
- console.log(` 4. cd ${originalCwd} && pnpm wu:done --id ${id}`);
1245
- console.log(`\n${PREFIX} Common mistakes to avoid:`);
1246
- console.log(` - Don't edit files on main branch`);
1247
- console.log(` - Don't manually edit WU YAML status fields`);
1248
- console.log(` - Don't create PRs (trunk-based development)`);
1249
- // WU-1501: Hint for sub-agent execution context
1250
- console.log(`\n${PREFIX} For sub-agent execution:`);
1251
- console.log(` /wu-prompt ${id} (generates full context prompt)`);
1252
- // Emit mandatory agent advisory based on code_paths (WU-1324)
1253
- // Read from worktree since that's where the updated YAML is
1254
- const wtWUPathForAdvisory = path.join(worktreePath, WU_PATH);
1255
- const wuContent = await readFile(wtWUPathForAdvisory, {
1256
- encoding: FILE_SYSTEM.UTF8,
1257
- });
1258
- const wuDoc = parseYAML(wuContent);
1259
- const codePaths = wuDoc.code_paths || [];
1260
- emitMandatoryAgentAdvisory(codePaths, id);
1261
- // WU-1047: Emit agent-only project defaults from config
1262
- const config = getConfig();
1263
- printProjectDefaults(config?.agents?.methodology);
1264
- // WU-1763: Print lifecycle nudge with tips for tool adoption
1265
- printLifecycleNudge(id);
1266
- // WU-1473: Surface unread coordination signals so agents see pending messages
1267
- // Fail-open: surfaceUnreadSignals never throws
1268
- await surfaceUnreadSignalsForDisplay(originalCwd);
1269
- }
1270
- /**
1271
- * WU-1047: Format Project Defaults section (agent-only).
1272
- *
1273
- * @param {object} methodology - Methodology defaults config
1274
- * @returns {string} Formatted output or empty string if disabled
1275
- */
1276
- export function formatProjectDefaults(methodology) {
1277
- if (!methodology || methodology.enabled === false)
1278
- return '';
1279
- const enforcement = methodology.enforcement || 'required';
1280
- const principles = Array.isArray(methodology.principles) ? methodology.principles : [];
1281
- const lines = [
1282
- `${PREFIX} 🧭 Project Defaults (agent-only)`,
1283
- ` Enforcement: ${enforcement}`,
1284
- ` Principles: ${principles.length > 0 ? principles.join(', ') : 'None'}`,
1285
- ];
1286
- if (methodology.notes) {
1287
- lines.push(` Notes: ${methodology.notes}`);
1288
- }
1289
- return `\n${lines.join('\n')}`;
1290
- }
1291
- /**
1292
- * WU-1047: Print Project Defaults section (agent-only).
1293
- *
1294
- * @param {object} methodology - Methodology defaults config
1295
- */
1296
- export function printProjectDefaults(methodology) {
1297
- const output = formatProjectDefaults(methodology);
1298
- if (output) {
1299
- console.log(output);
1300
- }
1301
- }
1302
- /**
1303
- * WU-1763: Print a single concise tips line to improve tool adoption.
1304
- * Non-blocking, single-line output to avoid flooding the console.
1305
- *
1306
- * @param {string} _id - WU ID being claimed (unused, kept for future use)
1307
- */
1308
- export function printLifecycleNudge(_id) {
1309
- // Single line, concise, actionable
1310
- console.log(`\n${PREFIX} 💡 Tip: pnpm session:recommend for context tier, mem:ready for pending work, pnpm file:*/git:* for audited wrappers`);
1311
- }
1312
- /**
1313
- * WU-1029: Apply symlink fallback (root + nested node_modules) after install failure.
1314
- *
1315
- * @param {string} worktreePath - Worktree path
1316
- * @param {string} mainRepoPath - Main repo path
1317
- * @param {Console} logger - Logger (console-compatible)
1318
- */
1319
- export function applyFallbackSymlinks(worktreePath, mainRepoPath, logger = console) {
1320
- const symlinkResult = symlinkNodeModules(worktreePath, logger, mainRepoPath);
1321
- if (symlinkResult.created) {
1322
- logger.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked as fallback`);
1323
- }
1324
- let nestedResult = null;
1325
- if (!symlinkResult.refused) {
1326
- nestedResult = symlinkNestedNodeModules(worktreePath, mainRepoPath);
1327
- if (nestedResult.created > 0) {
1328
- logger.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
1329
- }
1330
- }
1331
- return { symlinkResult, nestedResult };
1332
- }
1333
- /**
1334
- * WU-2411: Handle --resume flag for agent handoff
1335
- *
1336
- * When an agent crashes or is killed, the --resume flag allows a new agent
1337
- * to take over by:
1338
- * 1. Verifying the old PID is dead (safety check)
1339
- * 2. Updating the lock file with the new PID
1340
- * 3. Preserving the existing worktree
1341
- * 4. Printing uncommitted changes summary
1342
- * 5. Creating a checkpoint in the memory layer
1343
- *
1344
- * @param {Object} args - CLI arguments
1345
- * @param {string} id - WU ID
1346
- */
1347
- async function handleResumeMode(args, id) {
1348
- const laneK = toKebab(args.lane);
1349
- const idK = id.toLowerCase();
1350
- const worktree = args.worktree || `worktrees/${laneK}-${idK}`;
1351
- const worktreePath = path.resolve(worktree);
1352
- console.log(`${PREFIX} Attempting to resume ${id} in lane "${args.lane}"...`);
1353
- // Attempt the resume/handoff
1354
- const result = await resumeClaimForHandoff({
1355
- wuId: id,
1356
- lane: args.lane,
1357
- worktreePath,
1358
- agentSession: null, // Will be populated by session system
1359
- });
1360
- if (!result.success) {
1361
- die(`Cannot resume ${id}: ${result.error}\n\n` +
1362
- `If you need to start a fresh claim, use: pnpm wu:claim --id ${id} --lane "${args.lane}"`);
1363
- }
1364
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Handoff successful`);
1365
- console.log(`${PREFIX} Previous PID: ${result.previousPid}`);
1366
- console.log(`${PREFIX} New PID: ${process.pid}`);
1367
- // Get and display uncommitted changes in the worktree
1368
- const wtGit = createGitForPath(worktreePath);
1369
- const uncommittedStatus = await getWorktreeUncommittedChanges(wtGit);
1370
- if (uncommittedStatus) {
1371
- const formatted = formatUncommittedChanges(uncommittedStatus);
1372
- console.log(`\n${PREFIX} ${formatted}`);
1373
- }
1374
- else {
1375
- console.log(`\n${PREFIX} No uncommitted changes in worktree.`);
1376
- }
1377
- // Create handoff checkpoint in memory layer
1378
- const checkpointResult = await createHandoffCheckpoint({
1379
- wuId: id,
1380
- previousPid: result.previousPid,
1381
- newPid: process.pid,
1382
- previousSession: result.previousSession,
1383
- uncommittedSummary: uncommittedStatus,
1384
- });
1385
- if (checkpointResult.success && checkpointResult.checkpointId) {
1386
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Handoff checkpoint created: ${checkpointResult.checkpointId}`);
1387
- }
1388
- // Emit telemetry event for handoff
1389
- emitWUFlowEvent({
1390
- script: 'wu-claim',
1391
- wu_id: id,
1392
- lane: args.lane,
1393
- step: 'resume_handoff',
1394
- previousPid: result.previousPid,
1395
- newPid: process.pid,
1396
- uncommittedChanges: uncommittedStatus ? 'present' : 'none',
1397
- });
1398
- // Print summary
1399
- console.log(`\n${PREFIX} Resume complete. Worktree preserved at: ${worktree}`);
1400
- console.log(`${PREFIX} Next: cd ${worktree} and continue work.`);
1401
- console.log(`\n${PREFIX} Tip: Run 'pnpm mem:ready --wu ${id}' to check for pending context from previous session.`);
1402
- }
89
+ // ============================================================================
90
+ // Main orchestrator
91
+ // ============================================================================
1403
92
  // eslint-disable-next-line sonarjs/cognitive-complexity -- main() orchestrates multi-step claim workflow
1404
93
  async function main() {
1405
94
  const args = createWUParser({
@@ -1480,64 +169,23 @@ async function main() {
1480
169
  stagedChanges = await getStagedChanges();
1481
170
  }
1482
171
  // WU-1361: Fetch latest remote before validation (no local main mutation)
1483
- if (!args.noPush) {
172
+ // WU-1653: Also skip when git.requireRemote=false (local-only mode)
173
+ const skipRemote = shouldSkipRemoteOperations();
174
+ if (!args.noPush && !skipRemote) {
1484
175
  await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
1485
176
  await ensureMainUpToDate(getGitForCwd(), 'wu:claim');
1486
177
  }
178
+ else if (skipRemote) {
179
+ console.log(`${PREFIX} Local-only mode (git.requireRemote=false): skipping origin sync`);
180
+ }
1487
181
  else {
1488
182
  console.warn(`${PREFIX} Warning: --no-push enabled. Skipping origin/main sync; local state may be stale.`);
1489
183
  }
1490
184
  const WU_PATH = WU_PATHS.WU(id);
1491
185
  const STATUS_PATH = WU_PATHS.STATUS();
1492
186
  const BACKLOG_PATH = WU_PATHS.BACKLOG();
1493
- // PRE-FLIGHT VALIDATION (on post-pull data)
1494
- const doc = preflightValidateWU(WU_PATH, id);
1495
- const manualTestsCheck = validateManualTestsForClaim(doc, id);
1496
- if (!manualTestsCheck.valid) {
1497
- die(manualTestsCheck.error);
1498
- }
1499
- await handleOrphanCheck(args.lane, id);
1500
- validateLaneFormatWithError(args.lane);
1501
- // WU-1187: Check for WIP justification when WIP > 1 (soft enforcement - warning only)
1502
- const wipJustificationCheck = checkWipJustification(args.lane);
1503
- if (wipJustificationCheck.warning) {
1504
- console.warn(`${PREFIX} ${wipJustificationCheck.warning}`);
1505
- }
1506
- // WU-1372: Lane-to-code_paths consistency check (advisory only, never blocks)
1507
- const laneValidation = validateLaneCodePaths(doc, args.lane);
1508
- logLaneValidationWarnings(laneValidation, PREFIX);
1509
- // WU-1361: YAML schema validation at claim time
1510
- // Returns fixable issues for application in worktree (not on main)
1511
- const fixableIssues = validateYAMLSchema(WU_PATH, doc, args);
1512
- // WU-1506/WU-1576: Backlog invariant repair moved inside micro-worktree (see claimWorktreeMode)
1513
- // Previously called validateBacklogConsistency(BACKLOG_PATH) here which modified main directly
1514
- // WU-1362: Spec completeness validation (fail-fast before expensive operations)
1515
- // Two-tier validation: Schema errors (above) are never bypassable; spec completeness is bypassable
1516
- const specResult = validateSpecCompleteness(doc, id);
1517
- if (!specResult.valid) {
1518
- const errorList = specResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE);
1519
- if (args.allowIncomplete) {
1520
- console.warn(`${PREFIX} ⚠️ Spec completeness warnings (bypassed with --allow-incomplete):`);
1521
- console.warn(errorList);
1522
- console.warn(`${PREFIX} Proceeding with incomplete spec. Fix before wu:done.`);
1523
- }
1524
- else {
1525
- die(`Spec completeness validation failed for ${WU_PATH}:\n\n${errorList}\n\n` +
1526
- `Fix these issues before claiming, or use --allow-incomplete to bypass.\n` +
1527
- `Note: Schema errors (placeholders, invalid structure) cannot be bypassed.`);
1528
- }
1529
- }
1530
- // Check lane occupancy (WIP=1 per sub-lane)
1531
- const laneCheck = checkLaneFree(STATUS_PATH, args.lane, id);
1532
- emitWUFlowEvent({
1533
- script: 'wu-claim',
1534
- wu_id: id,
1535
- lane: args.lane,
1536
- step: 'lane_check',
1537
- occupied: !laneCheck.free,
1538
- occupiedBy: laneCheck.occupiedBy,
1539
- });
1540
- handleLaneOccupancy(laneCheck, args.lane, id, args.force);
187
+ // WU-1649: Delegated to wu-claim-validation.ts
188
+ const { fixableIssues } = await runPreflightValidations(args, id, WU_PATH, STATUS_PATH);
1541
189
  // WU-1603: Atomic lane lock to prevent TOCTOU race conditions
1542
190
  // This is Layer 2 defense after status.md check - prevents parallel agents from
1543
191
  // both reading a free status.md before either updates it
@@ -1617,7 +265,8 @@ async function main() {
1617
265
  laneBranch: effectiveBranch,
1618
266
  });
1619
267
  // Check if remote branch already exists (prevents duplicate global claims)
1620
- if (!args.noPush && !skipBranchChecks) {
268
+ // WU-1653: Skip when requireRemote=false (no remote to check)
269
+ if (!args.noPush && !skipBranchChecks && !shouldSkipRemoteOperations()) {
1621
270
  const remoteExists = await getGitForCwd().remoteBranchExists(REMOTES.ORIGIN, effectiveBranch);
1622
271
  if (remoteExists) {
1623
272
  die(`Remote branch ${REMOTES.ORIGIN}/${effectiveBranch} already exists. WU may already be claimed.\n\n` +
@@ -1693,7 +342,7 @@ async function main() {
1693
342
  };
1694
343
  let updatedTitle = title;
1695
344
  claimTitle = title;
1696
- const shouldApplyCanonicalUpdate = shouldApplyCanonicalClaimUpdate({
345
+ const shouldApplyCanonicalUpdate = shouldApplyCanonicalClaimUpdateFn({
1697
346
  isCloud: effectiveCloud,
1698
347
  claimedMode,
1699
348
  noPush: Boolean(args.noPush),
@@ -1705,7 +354,10 @@ async function main() {
1705
354
  canonicalClaimPushed = true;
1706
355
  claimTitle = updatedTitle || title;
1707
356
  // Refresh origin/main after push-only update so worktrees start from canonical state
1708
- await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
357
+ // WU-1653: Skip fetch when requireRemote=false (no remote)
358
+ if (!shouldSkipRemoteOperations()) {
359
+ await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
360
+ }
1709
361
  }
1710
362
  else if (!args.noPush && claimedMode === CLAIMED_MODES.BRANCH_PR) {
1711
363
  console.log(`${PREFIX} Skipping canonical claim update on origin/main for cloud branch-pr claim.`);