@lumenflow/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +116 -0
  3. package/dist/gates.d.ts +41 -0
  4. package/dist/gates.d.ts.map +1 -0
  5. package/dist/gates.js +684 -0
  6. package/dist/gates.js.map +1 -0
  7. package/dist/initiative-add-wu.d.ts +22 -0
  8. package/dist/initiative-add-wu.d.ts.map +1 -0
  9. package/dist/initiative-add-wu.js +234 -0
  10. package/dist/initiative-add-wu.js.map +1 -0
  11. package/dist/initiative-create.d.ts +28 -0
  12. package/dist/initiative-create.d.ts.map +1 -0
  13. package/dist/initiative-create.js +172 -0
  14. package/dist/initiative-create.js.map +1 -0
  15. package/dist/initiative-edit.d.ts +34 -0
  16. package/dist/initiative-edit.d.ts.map +1 -0
  17. package/dist/initiative-edit.js +440 -0
  18. package/dist/initiative-edit.js.map +1 -0
  19. package/dist/initiative-list.d.ts +12 -0
  20. package/dist/initiative-list.d.ts.map +1 -0
  21. package/dist/initiative-list.js +101 -0
  22. package/dist/initiative-list.js.map +1 -0
  23. package/dist/initiative-status.d.ts +11 -0
  24. package/dist/initiative-status.d.ts.map +1 -0
  25. package/dist/initiative-status.js +221 -0
  26. package/dist/initiative-status.js.map +1 -0
  27. package/dist/mem-checkpoint.d.ts +16 -0
  28. package/dist/mem-checkpoint.d.ts.map +1 -0
  29. package/dist/mem-checkpoint.js +237 -0
  30. package/dist/mem-checkpoint.js.map +1 -0
  31. package/dist/mem-cleanup.d.ts +29 -0
  32. package/dist/mem-cleanup.d.ts.map +1 -0
  33. package/dist/mem-cleanup.js +267 -0
  34. package/dist/mem-cleanup.js.map +1 -0
  35. package/dist/mem-create.d.ts +17 -0
  36. package/dist/mem-create.d.ts.map +1 -0
  37. package/dist/mem-create.js +265 -0
  38. package/dist/mem-create.js.map +1 -0
  39. package/dist/mem-inbox.d.ts +35 -0
  40. package/dist/mem-inbox.d.ts.map +1 -0
  41. package/dist/mem-inbox.js +373 -0
  42. package/dist/mem-inbox.js.map +1 -0
  43. package/dist/mem-init.d.ts +15 -0
  44. package/dist/mem-init.d.ts.map +1 -0
  45. package/dist/mem-init.js +146 -0
  46. package/dist/mem-init.js.map +1 -0
  47. package/dist/mem-ready.d.ts +16 -0
  48. package/dist/mem-ready.d.ts.map +1 -0
  49. package/dist/mem-ready.js +224 -0
  50. package/dist/mem-ready.js.map +1 -0
  51. package/dist/mem-signal.d.ts +16 -0
  52. package/dist/mem-signal.d.ts.map +1 -0
  53. package/dist/mem-signal.js +204 -0
  54. package/dist/mem-signal.js.map +1 -0
  55. package/dist/mem-start.d.ts +16 -0
  56. package/dist/mem-start.d.ts.map +1 -0
  57. package/dist/mem-start.js +158 -0
  58. package/dist/mem-start.js.map +1 -0
  59. package/dist/mem-summarize.d.ts +22 -0
  60. package/dist/mem-summarize.d.ts.map +1 -0
  61. package/dist/mem-summarize.js +213 -0
  62. package/dist/mem-summarize.js.map +1 -0
  63. package/dist/mem-triage.d.ts +22 -0
  64. package/dist/mem-triage.d.ts.map +1 -0
  65. package/dist/mem-triage.js +328 -0
  66. package/dist/mem-triage.js.map +1 -0
  67. package/dist/spawn-list.d.ts +16 -0
  68. package/dist/spawn-list.d.ts.map +1 -0
  69. package/dist/spawn-list.js +140 -0
  70. package/dist/spawn-list.js.map +1 -0
  71. package/dist/wu-block.d.ts +16 -0
  72. package/dist/wu-block.d.ts.map +1 -0
  73. package/dist/wu-block.js +241 -0
  74. package/dist/wu-block.js.map +1 -0
  75. package/dist/wu-claim.d.ts +32 -0
  76. package/dist/wu-claim.d.ts.map +1 -0
  77. package/dist/wu-claim.js +1106 -0
  78. package/dist/wu-claim.js.map +1 -0
  79. package/dist/wu-cleanup.d.ts +17 -0
  80. package/dist/wu-cleanup.d.ts.map +1 -0
  81. package/dist/wu-cleanup.js +194 -0
  82. package/dist/wu-cleanup.js.map +1 -0
  83. package/dist/wu-create.d.ts +38 -0
  84. package/dist/wu-create.d.ts.map +1 -0
  85. package/dist/wu-create.js +520 -0
  86. package/dist/wu-create.js.map +1 -0
  87. package/dist/wu-deps.d.ts +13 -0
  88. package/dist/wu-deps.d.ts.map +1 -0
  89. package/dist/wu-deps.js +119 -0
  90. package/dist/wu-deps.js.map +1 -0
  91. package/dist/wu-done.d.ts +153 -0
  92. package/dist/wu-done.d.ts.map +1 -0
  93. package/dist/wu-done.js +2096 -0
  94. package/dist/wu-done.js.map +1 -0
  95. package/dist/wu-edit.d.ts +29 -0
  96. package/dist/wu-edit.d.ts.map +1 -0
  97. package/dist/wu-edit.js +852 -0
  98. package/dist/wu-edit.js.map +1 -0
  99. package/dist/wu-infer-lane.d.ts +17 -0
  100. package/dist/wu-infer-lane.d.ts.map +1 -0
  101. package/dist/wu-infer-lane.js +135 -0
  102. package/dist/wu-infer-lane.js.map +1 -0
  103. package/dist/wu-preflight.d.ts +47 -0
  104. package/dist/wu-preflight.d.ts.map +1 -0
  105. package/dist/wu-preflight.js +167 -0
  106. package/dist/wu-preflight.js.map +1 -0
  107. package/dist/wu-prune.d.ts +16 -0
  108. package/dist/wu-prune.d.ts.map +1 -0
  109. package/dist/wu-prune.js +259 -0
  110. package/dist/wu-prune.js.map +1 -0
  111. package/dist/wu-repair.d.ts +60 -0
  112. package/dist/wu-repair.d.ts.map +1 -0
  113. package/dist/wu-repair.js +226 -0
  114. package/dist/wu-repair.js.map +1 -0
  115. package/dist/wu-spawn-completion.d.ts +10 -0
  116. package/dist/wu-spawn-completion.js +30 -0
  117. package/dist/wu-spawn.d.ts +168 -0
  118. package/dist/wu-spawn.d.ts.map +1 -0
  119. package/dist/wu-spawn.js +1327 -0
  120. package/dist/wu-spawn.js.map +1 -0
  121. package/dist/wu-unblock.d.ts +16 -0
  122. package/dist/wu-unblock.d.ts.map +1 -0
  123. package/dist/wu-unblock.js +234 -0
  124. package/dist/wu-unblock.js.map +1 -0
  125. package/dist/wu-validate.d.ts +16 -0
  126. package/dist/wu-validate.d.ts.map +1 -0
  127. package/dist/wu-validate.js +193 -0
  128. package/dist/wu-validate.js.map +1 -0
  129. package/package.json +92 -0
@@ -0,0 +1,1106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Claim Helper
4
+ *
5
+ * Canonical sequence:
6
+ * 1) Auto-update backlog/status/WU YAML (safe parsing) unless `--no-auto`
7
+ * 2) Commit and push to `main`
8
+ * 3) Create a dedicated worktree+branch for the WU
9
+ *
10
+ * Usage:
11
+ * node tools/wu-claim.mjs --id WU-334 --lane Intelligence \
12
+ * [--worktree worktrees/intelligence-wu-334] [--branch lane/intelligence/wu-334]
13
+ *
14
+ * WU-2542: This script imports utilities from @lumenflow/core package.
15
+ * Full migration to thin shim pending @lumenflow/core CLI export implementation.
16
+ */
17
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
18
+ import { access, readFile, writeFile } from 'node:fs/promises';
19
+ import path from 'node:path';
20
+ import { isOrphanWorktree } from '@lumenflow/core/dist/orphan-detector.js';
21
+ // WU-1352: Use centralized YAML functions from wu-yaml.mjs
22
+ import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
23
+ import { assertTransition } from '@lumenflow/core/dist/state-machine.js';
24
+ import { checkLaneFree, validateLaneFormat } from '@lumenflow/core/dist/lane-checker.js';
25
+ // WU-1603: Atomic lane locking to prevent TOCTOU race conditions
26
+ import { acquireLaneLock, releaseLaneLock, checkLaneLock, forceRemoveStaleLock, } from '@lumenflow/core/dist/lane-lock.js';
27
+ // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
28
+ import { validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/dist/code-path-validator.js';
29
+ // WU-1574: parseBacklogFrontmatter/getSectionHeadings removed - state store replaces backlog parsing
30
+ import { detectConflicts } from '@lumenflow/core/dist/code-paths-overlap.js';
31
+ import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
32
+ import { die } from '@lumenflow/core/dist/error-handler.js';
33
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
34
+ import { WU_PATHS, getStateStoreDirFromBacklog } from '@lumenflow/core/dist/wu-paths.js';
35
+ import { BRANCHES, REMOTES, WU_STATUS, CLAIMED_MODES, STATUS_SECTIONS, PATTERNS, toKebab, LOG_PREFIX, MICRO_WORKTREE_OPERATIONS, COMMIT_FORMATS, EMOJI, FILE_SYSTEM, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
36
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
37
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
38
+ import { emitWUFlowEvent } from '@lumenflow/core/dist/telemetry.js';
39
+ import { checkLaneForOrphanDoneWU, repairWUInconsistency, } from '@lumenflow/core/dist/wu-consistency-checker.js';
40
+ import { emitMandatoryAgentAdvisory } from '@lumenflow/core/dist/orchestration-advisory-loader.js';
41
+ import { validateWU, generateAutoApproval } from '@lumenflow/core/dist/wu-schema.js';
42
+ import { startSessionForWU } from '@lumenflow/agent/dist/auto-session-integration.js';
43
+ import { detectFixableIssues, applyFixes, autoFixWUYaml, formatIssues, } from '@lumenflow/core/dist/wu-yaml-fixer.js';
44
+ import { validateSpecCompleteness } from '@lumenflow/core/dist/wu-done-validators.js';
45
+ import { getAssignedEmail } from '@lumenflow/core/dist/wu-claim-helpers.js';
46
+ import { symlinkNodeModules, symlinkNestedNodeModules, } from '@lumenflow/core/dist/worktree-symlink.js';
47
+ // WU-1572: Import WUStateStore for event-sourced state tracking
48
+ import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
49
+ // WU-1574: Import backlog generator to replace BacklogManager
50
+ import { generateBacklog, generateStatus } from '@lumenflow/core/dist/backlog-generator.js';
51
+ // WU-2411: Import resume helpers for agent handoff
52
+ import { resumeClaimForHandoff, getWorktreeUncommittedChanges, formatUncommittedChanges, createHandoffCheckpoint, } from '@lumenflow/core/dist/wu-claim-resume.js';
53
+ // ensureOnMain() moved to wu-helpers.mjs (WU-1256)
54
+ async function ensureCleanOrClaimOnlyWhenNoAuto() {
55
+ // Require staged claim edits only if running with --no-auto
56
+ const status = await getGitForCwd().getStatus();
57
+ if (!status)
58
+ die('No staged changes detected. Stage backlog/status/WU YAML claim edits first or omit --no-auto.');
59
+ const staged = status
60
+ .split(STRING_LITERALS.NEWLINE)
61
+ .filter(Boolean)
62
+ .filter((l) => l.startsWith('A ') || l.startsWith('M ') || l.startsWith('R '));
63
+ const hasClaimFiles = staged.some((l) => l.includes('docs/04-operations/tasks/status.md') ||
64
+ l.includes('docs/04-operations/tasks/backlog.md') ||
65
+ /docs\/04-operations\/tasks\/wu\/WU-\d+\.yaml/.test(l));
66
+ if (!hasClaimFiles) {
67
+ console.error(status);
68
+ die('Stage claim-related files (status/backlog/WU YAML) before running with --no-auto.');
69
+ }
70
+ }
71
+ const PREFIX = LOG_PREFIX.CLAIM;
72
+ /**
73
+ * Pre-flight validation: Check WU file exists and is valid BEFORE any git operations
74
+ * Prevents zombie worktrees when WU YAML is missing or malformed
75
+ */
76
+ function preflightValidateWU(WU_PATH, id) {
77
+ // Check file exists
78
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
79
+ if (!existsSync(WU_PATH)) {
80
+ die(`WU file not found: ${WU_PATH}\n\n` +
81
+ `Cannot claim a WU that doesn't exist.\n\n` +
82
+ `Options:\n` +
83
+ ` 1. Create the WU first: pnpm wu:create --id ${id} --lane <lane> --title "..."\n` +
84
+ ` 2. Check if the WU ID is correct\n` +
85
+ ` 3. Check if the WU file was moved or deleted`);
86
+ }
87
+ // Parse and validate YAML structure
88
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
89
+ const text = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
90
+ let doc;
91
+ try {
92
+ doc = parseYAML(text);
93
+ }
94
+ catch (e) {
95
+ die(`Failed to parse WU YAML ${WU_PATH}\n\n` +
96
+ `YAML parsing error: ${e.message}\n\n` +
97
+ `Fix the YAML syntax errors before claiming.`);
98
+ }
99
+ // Validate ID matches
100
+ if (!doc || doc.id !== id) {
101
+ die(`WU YAML id mismatch in ${WU_PATH}\n\n` +
102
+ `Expected: ${id}\n` +
103
+ `Found: ${doc?.id || 'missing'}\n\n` +
104
+ `Fix the id field in the WU YAML before claiming.`);
105
+ }
106
+ // Validate state transition is allowed
107
+ const currentStatus = doc.status || WU_STATUS.READY;
108
+ try {
109
+ assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
110
+ }
111
+ catch (error) {
112
+ die(`Cannot claim ${id} - invalid state transition\n\n` +
113
+ `Current status: ${currentStatus}\n` +
114
+ `Attempted transition: ${currentStatus} → in_progress\n\n` +
115
+ `Reason: ${error.message}`);
116
+ }
117
+ return doc;
118
+ }
119
+ /**
120
+ * WU-1361: Validate YAML schema at claim time
121
+ *
122
+ * Validates WU YAML against Zod schema AFTER git pull.
123
+ * Detects fixable issues BEFORE schema validation (so --fix can run even if schema fails).
124
+ * Returns fixable issues for application in worktree (WU-1361 fix).
125
+ *
126
+ * @param {string} WU_PATH - Path to WU YAML file
127
+ * @param {object} doc - Parsed WU YAML data
128
+ * @param {object} args - CLI arguments
129
+ * @param {boolean} args.fix - If true, issues will be fixed in worktree
130
+ * @returns {Array} Array of fixable issues to apply in worktree
131
+ */
132
+ function validateYAMLSchema(WU_PATH, doc, args) {
133
+ // WU-1361: Detect fixable issues BEFORE schema validation
134
+ // This allows --fix to work even when schema would fail
135
+ const fixableIssues = detectFixableIssues(doc);
136
+ if (fixableIssues.length > 0) {
137
+ if (args.fix) {
138
+ // WU-1425: Apply fixes to in-memory doc so validation passes
139
+ // Note: This does NOT modify the file on disk - only the in-memory object
140
+ // The actual file fix happens when the doc is written to the worktree
141
+ applyFixes(doc, fixableIssues);
142
+ console.log(`${PREFIX} Detected ${fixableIssues.length} fixable YAML issue(s) (will fix in worktree):`);
143
+ console.log(formatIssues(fixableIssues));
144
+ }
145
+ else {
146
+ // Report issues and suggest --fix
147
+ console.warn(`${PREFIX} Detected ${fixableIssues.length} fixable YAML issue(s):`);
148
+ console.warn(formatIssues(fixableIssues));
149
+ console.warn(`${PREFIX} Run with --fix to auto-repair these issues.`);
150
+ // Continue - Zod validation will provide the detailed error
151
+ }
152
+ }
153
+ // Now run Zod schema validation
154
+ const schemaResult = validateWU(doc);
155
+ if (!schemaResult.success) {
156
+ const issueList = schemaResult.error.issues
157
+ .map((i) => ` - ${i.path.join('.')}: ${i.message}`)
158
+ .join(STRING_LITERALS.NEWLINE);
159
+ const tip = fixableIssues.length > 0 ? 'Tip: Run with --fix to auto-repair common issues.\n' : '';
160
+ die(`WU YAML schema validation failed for ${WU_PATH}:\n\n${issueList}\n\nFix these issues before claiming.\n${tip}`);
161
+ }
162
+ // WU-1361: Return fixable issues for application in worktree
163
+ return args.fix ? fixableIssues : [];
164
+ }
165
+ // WU-1576: validateBacklogConsistency removed - repair now happens inside micro-worktree
166
+ // See claimWorktreeMode() execute function for the new location
167
+ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null) {
168
+ // Check file exists
169
+ try {
170
+ await access(WU_PATH);
171
+ }
172
+ catch {
173
+ die(`WU file not found: ${WU_PATH}\n\n` +
174
+ `Options:\n` +
175
+ ` 1. Create the WU first: pnpm wu:create --id ${id} --lane "${lane}" --title "..."\n` +
176
+ ` 2. Check if the WU ID is correct`);
177
+ }
178
+ // Read file
179
+ let text;
180
+ try {
181
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
182
+ text = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
183
+ }
184
+ catch (e) {
185
+ die(`Failed to read WU file: ${WU_PATH}\n\n` +
186
+ `Error: ${e.message}\n\n` +
187
+ `Options:\n` +
188
+ ` 1. Check file permissions: ls -la ${WU_PATH}\n` +
189
+ ` 2. Ensure you have read access to the repository`);
190
+ }
191
+ let doc;
192
+ try {
193
+ doc = parseYAML(text);
194
+ }
195
+ catch (e) {
196
+ die(`Failed to parse YAML ${WU_PATH}\n\n` +
197
+ `Error: ${e.message}\n\n` +
198
+ `Options:\n` +
199
+ ` 1. Validate YAML syntax: pnpm wu:validate --id ${id}\n` +
200
+ ` 2. Fix YAML errors manually and retry`);
201
+ }
202
+ if (!doc || doc.id !== id) {
203
+ die(`WU YAML id mismatch. Expected ${id}, found ${doc && doc.id}\n\n` +
204
+ `Options:\n` +
205
+ ` 1. Check the WU file has correct id field\n` +
206
+ ` 2. Verify you're claiming the right WU`);
207
+ }
208
+ // Validate state transition before updating
209
+ const currentStatus = doc.status || WU_STATUS.READY;
210
+ try {
211
+ assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
212
+ }
213
+ catch (error) {
214
+ die(`State transition validation failed: ${error.message}`);
215
+ }
216
+ // Update status and lane (lane only if provided and different)
217
+ doc.status = WU_STATUS.IN_PROGRESS;
218
+ if (lane)
219
+ doc.lane = lane;
220
+ // Record claimed mode (worktree or branch-only)
221
+ doc.claimed_mode = claimedMode;
222
+ // WU-1226: Record worktree path to prevent resolution failures if lane field changes
223
+ if (worktreePath) {
224
+ doc.worktree_path = worktreePath;
225
+ }
226
+ // WU-1423: Record owner using validated email (no silent username fallback)
227
+ // Fallback chain: git config user.email > GIT_AUTHOR_EMAIL > error
228
+ // WU-1427: getAssignedEmail is now async to properly await gitAdapter.getConfigValue
229
+ doc.assigned_to = await getAssignedEmail(getGitForCwd());
230
+ // Record claim timestamp for duration tracking (WU-637)
231
+ doc.claimed_at = new Date().toISOString();
232
+ // WU-1382: Store baseline main SHA for parallel agent detection
233
+ // wu:done will compare against this to detect if other WUs were merged during work
234
+ const git = getGitForCwd();
235
+ doc.baseline_main_sha = await git.getCommitHash('origin/main');
236
+ // WU-1438: Store agent session ID for tracking
237
+ if (sessionId) {
238
+ doc.session_id = sessionId;
239
+ }
240
+ // WU-2080: Agent-first auto-approval
241
+ // Agents auto-approve on claim. Human escalation only for detected triggers.
242
+ const autoApproval = generateAutoApproval(doc, doc.assigned_to);
243
+ doc.approved_by = autoApproval.approved_by;
244
+ doc.approved_at = autoApproval.approved_at;
245
+ doc.escalation_triggers = autoApproval.escalation_triggers;
246
+ doc.requires_human_escalation = autoApproval.requires_human_escalation;
247
+ // Log escalation triggers if any detected
248
+ if (autoApproval.requires_human_escalation) {
249
+ console.log(`[wu-claim] ⚠️ Escalation triggers detected: ${autoApproval.escalation_triggers.join(', ')}`);
250
+ console.log(`[wu-claim] ℹ️ Human resolution required before wu:done can complete.`);
251
+ }
252
+ else {
253
+ console.log(`[wu-claim] ✅ Agent auto-approved (no escalation triggers)`);
254
+ }
255
+ // WU-1352: Use centralized stringify for consistent output
256
+ const out = stringifyYAML(doc);
257
+ // Write file
258
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
259
+ await writeFile(WU_PATH, out, { encoding: FILE_SYSTEM.UTF8 });
260
+ return doc.title || '';
261
+ }
262
+ async function addOrReplaceInProgressStatus(statusPath, id, title) {
263
+ // Check file exists
264
+ try {
265
+ await access(statusPath);
266
+ }
267
+ catch {
268
+ die(`Missing ${statusPath}`);
269
+ }
270
+ const rel = `wu/${id}.yaml`;
271
+ const bullet = `- [${id} — ${title}](${rel})`;
272
+ // Read file
273
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
274
+ const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
275
+ const lines = content.split(STRING_LITERALS.NEWLINE);
276
+ const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
277
+ const startIdx = findHeader(STATUS_SECTIONS.IN_PROGRESS);
278
+ if (startIdx === -1)
279
+ die(`Could not find "${STATUS_SECTIONS.IN_PROGRESS}" section in status.md`);
280
+ let endIdx = lines.slice(startIdx + 1).findIndex((l) => l.startsWith('## '));
281
+ if (endIdx === -1)
282
+ endIdx = lines.length - startIdx - 1;
283
+ else
284
+ endIdx = startIdx + 1 + endIdx;
285
+ // Check if already present
286
+ const section = lines.slice(startIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
287
+ if (section.includes(rel) || section.includes(`[${id}`))
288
+ return; // already listed
289
+ // Remove "No items" marker if present
290
+ for (let i = startIdx + 1; i < endIdx; i++) {
291
+ // eslint-disable-next-line security/detect-object-injection -- array index loop
292
+ if (lines[i] && lines[i].includes('No items currently in progress')) {
293
+ lines.splice(i, 1);
294
+ endIdx--;
295
+ break;
296
+ }
297
+ }
298
+ // Insert bullet right after header
299
+ lines.splice(startIdx + 1, 0, '', bullet);
300
+ // Write file
301
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes status file
302
+ await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
303
+ encoding: FILE_SYSTEM.UTF8,
304
+ });
305
+ }
306
+ async function removeFromReadyAndAddToInProgressBacklog(backlogPath, id, title, lane) {
307
+ // WU-1574: Use WUStateStore as single source of truth, generate backlog.md from state
308
+ // WU-1593: Use centralized path helper to correctly resolve state dir from backlog path
309
+ const stateDir = getStateStoreDirFromBacklog(backlogPath);
310
+ // Append claim event to state store
311
+ const store = new WUStateStore(stateDir);
312
+ await store.load();
313
+ await store.claim(id, lane, title);
314
+ console.log(`${PREFIX} Claim event appended to state store`);
315
+ // Regenerate backlog.md from state store
316
+ const backlogContent = await generateBacklog(store);
317
+ await writeFile(backlogPath, backlogContent, { encoding: FILE_SYSTEM.UTF8 });
318
+ console.log(`${PREFIX} backlog.md regenerated from state store`);
319
+ // Regenerate status.md from state store
320
+ const statusPath = path.join(path.dirname(backlogPath), 'status.md');
321
+ const statusContent = await generateStatus(store);
322
+ await writeFile(statusPath, statusContent, { encoding: FILE_SYSTEM.UTF8 });
323
+ console.log(`${PREFIX} status.md regenerated from state store`);
324
+ }
325
+ /**
326
+ * WU-1746: Append claim event without regenerating backlog.md/status.md
327
+ * For worktree mode, we only need to record the claim event in the state store.
328
+ * Generated files (backlog.md, status.md) cause merge conflicts when committed
329
+ * to worktrees because they change on main as other WUs complete.
330
+ *
331
+ * @param {string} stateDir - Path to state store directory
332
+ * @param {string} id - WU ID
333
+ * @param {string} title - WU title
334
+ * @param {string} lane - Lane name
335
+ */
336
+ async function appendClaimEventOnly(stateDir, id, title, lane) {
337
+ const store = new WUStateStore(stateDir);
338
+ await store.load();
339
+ await store.claim(id, lane, title);
340
+ console.log(`${PREFIX} Claim event appended to state store`);
341
+ }
342
+ /**
343
+ * WU-1746: Get list of files to commit in worktree mode
344
+ * Excludes backlog.md and status.md to prevent merge conflicts.
345
+ * These generated files should only be updated on main during wu:done.
346
+ *
347
+ * @param {string} wuId - WU ID (e.g., 'WU-1746')
348
+ * @returns {string[]} List of files to commit
349
+ */
350
+ export function getWorktreeCommitFiles(wuId) {
351
+ return [
352
+ `docs/04-operations/tasks/wu/${wuId}.yaml`,
353
+ '.beacon/state/wu-events.jsonl', // WU-1740: Event store is source of truth
354
+ // WU-1746: Explicitly NOT including:
355
+ // - docs/04-operations/tasks/backlog.md
356
+ // - docs/04-operations/tasks/status.md
357
+ // These generated files cause merge conflicts when main advances
358
+ ];
359
+ }
360
+ async function readWUTitle(id) {
361
+ const p = WU_PATHS.WU(id);
362
+ // Check file exists
363
+ try {
364
+ await access(p);
365
+ }
366
+ catch {
367
+ return null;
368
+ }
369
+ // Read file
370
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
371
+ const text = await readFile(p, { encoding: FILE_SYSTEM.UTF8 });
372
+ const m = text.match(/^title:\s*"?(.+?)"?$/m);
373
+ return m ? m[1] : null;
374
+ }
375
+ // emitWUFlowEvent() moved to telemetry.mjs as emitWUFlowEvent() (WU-1256)
376
+ /**
377
+ * Check if there's already a Branch-Only WU in progress
378
+ * Branch-Only mode doesn't support parallel WUs (only one WU at a time in main checkout)
379
+ * @param {string} statusPath - Path to status.md
380
+ * @param {string} currentWU - Current WU ID being claimed
381
+ * @returns {Promise<{hasBranchOnly: boolean, existingWU: string|null}>}
382
+ */
383
+ async function checkExistingBranchOnlyWU(statusPath, currentWU) {
384
+ // Check file exists
385
+ try {
386
+ await access(statusPath);
387
+ }
388
+ catch {
389
+ return { hasBranchOnly: false, existingWU: null };
390
+ }
391
+ // Read file
392
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
393
+ const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
394
+ const lines = content.split(STRING_LITERALS.NEWLINE);
395
+ // Find "In Progress" section
396
+ const startIdx = lines.findIndex((l) => l.trim().toLowerCase() === '## in progress');
397
+ if (startIdx === -1)
398
+ return { hasBranchOnly: false, existingWU: null };
399
+ let endIdx = lines.slice(startIdx + 1).findIndex((l) => l.startsWith('## '));
400
+ if (endIdx === -1)
401
+ endIdx = lines.length - startIdx - 1;
402
+ else
403
+ endIdx = startIdx + 1 + endIdx;
404
+ // Extract WU IDs from In Progress section
405
+ const wuPattern = /\[?(WU-\d+)/i;
406
+ const inProgressWUs = lines
407
+ .slice(startIdx + 1, endIdx)
408
+ .map((line) => {
409
+ const match = line.match(wuPattern);
410
+ return match ? match[1].toUpperCase() : null;
411
+ })
412
+ .filter(Boolean)
413
+ .filter((wuid) => wuid !== currentWU); // exclude the WU we're claiming
414
+ // Check each in-progress WU for claimed_mode: branch-only
415
+ for (const wuid of inProgressWUs) {
416
+ const wuPath = WU_PATHS.WU(wuid);
417
+ // Check file exists
418
+ try {
419
+ await access(wuPath);
420
+ }
421
+ catch {
422
+ continue; // File doesn't exist, skip
423
+ }
424
+ try {
425
+ // Read file
426
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
427
+ const text = await readFile(wuPath, { encoding: FILE_SYSTEM.UTF8 });
428
+ const doc = parseYAML(text);
429
+ if (doc && doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY) {
430
+ return { hasBranchOnly: true, existingWU: wuid };
431
+ }
432
+ }
433
+ catch {
434
+ // ignore parse errors
435
+ }
436
+ }
437
+ return { hasBranchOnly: false, existingWU: null };
438
+ }
439
+ /**
440
+ * Handle orphan WU check and auto-repair (WU-1276)
441
+ * WU-1426: Commits repair changes to avoid dirty working tree blocking claim
442
+ * WU-1437: Use pushOnly micro-worktree to keep local main pristine
443
+ */
444
+ async function handleOrphanCheck(lane, id) {
445
+ const orphanCheck = await checkLaneForOrphanDoneWU(lane, id);
446
+ if (orphanCheck.valid)
447
+ return;
448
+ // Try auto-repair for single orphan
449
+ if (orphanCheck.orphans.length === 1) {
450
+ const orphanId = orphanCheck.orphans[0];
451
+ console.log(`${PREFIX} Auto-repairing orphan: ${orphanId}`);
452
+ // WU-1437: Use micro-worktree with pushOnly to keep main pristine
453
+ await withMicroWorktree({
454
+ operation: MICRO_WORKTREE_OPERATIONS.ORPHAN_REPAIR,
455
+ id: orphanId,
456
+ logPrefix: PREFIX,
457
+ pushOnly: true,
458
+ execute: async ({ worktreePath }) => {
459
+ // Run repair inside micro-worktree using projectRoot option
460
+ const repairResult = await repairWUInconsistency(orphanCheck.reports[0], {
461
+ projectRoot: worktreePath,
462
+ });
463
+ if (repairResult.failed > 0) {
464
+ throw new Error(`Lane ${lane} has orphan done WU: ${orphanId}\n` +
465
+ `Auto-repair failed. Fix manually with: pnpm wu:repair --id ${orphanId}`);
466
+ }
467
+ if (repairResult.repaired === 0) {
468
+ // Nothing to repair - return empty result
469
+ return { commitMessage: null, files: [] };
470
+ }
471
+ // Return files for commit
472
+ // WU-1740: Include wu-events.jsonl to persist state store events
473
+ return {
474
+ commitMessage: `chore(repair): auto-repair orphan ${orphanId.toLowerCase()}`,
475
+ files: [
476
+ WU_PATHS.BACKLOG(),
477
+ WU_PATHS.STATUS(),
478
+ `.beacon/stamps/${orphanId}.done`,
479
+ '.beacon/state/wu-events.jsonl',
480
+ ],
481
+ };
482
+ },
483
+ });
484
+ console.log(`${PREFIX} Auto-repair successful`);
485
+ return;
486
+ }
487
+ die(`Lane ${lane} has ${orphanCheck.orphans.length} orphan done WUs: ${orphanCheck.orphans.join(', ')}\n` +
488
+ `Fix with: pnpm wu:repair --id <WU-ID> for each, or pnpm wu:repair --all`);
489
+ }
490
+ /**
491
+ * Validate lane format with user-friendly error messages
492
+ */
493
+ function validateLaneFormatWithError(lane) {
494
+ try {
495
+ validateLaneFormat(lane);
496
+ }
497
+ catch (error) {
498
+ die(`Invalid lane format: ${error.message}\n\n` +
499
+ `Valid formats:\n` +
500
+ ` - Parent-only: "Operations", "Intelligence", "Experience", etc.\n` +
501
+ ` - Sub-lane: "Operations: Tooling", "Intelligence: Prompts", etc.\n\n` +
502
+ `Format rules:\n` +
503
+ ` - Single colon with EXACTLY one space after (e.g., "Parent: Subdomain")\n` +
504
+ ` - No spaces before colon\n` +
505
+ ` - No multiple colons\n\n` +
506
+ `See .lumenflow.config.yaml for valid parent lanes.`);
507
+ }
508
+ }
509
+ /**
510
+ * Handle lane occupancy check and enforce WIP=1 policy
511
+ */
512
+ function handleLaneOccupancy(laneCheck, lane, id, force) {
513
+ if (laneCheck.free)
514
+ return;
515
+ if (laneCheck.error) {
516
+ die(`Lane check failed: ${laneCheck.error}`);
517
+ }
518
+ if (!laneCheck.occupiedBy)
519
+ return;
520
+ if (force) {
521
+ console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}" is occupied by ${laneCheck.occupiedBy}`);
522
+ console.warn(`${PREFIX} ⚠️ Forcing WIP=2 in same lane. Risk of worktree collision!`);
523
+ console.warn(`${PREFIX} ⚠️ Use only for P0 emergencies or manual recovery.`);
524
+ return;
525
+ }
526
+ die(`Lane "${lane}" is already occupied by ${laneCheck.occupiedBy}.\n\n` +
527
+ `LumenFlow enforces one-WU-per-lane to maintain focus.\n\n` +
528
+ `Options:\n` +
529
+ ` 1. Wait for ${laneCheck.occupiedBy} to complete or block\n` +
530
+ ` 2. Choose a different lane\n` +
531
+ ` 3. Use --force to override (P0 emergencies only)\n\n` +
532
+ `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
533
+ }
534
+ /**
535
+ * Handle code path overlap detection (WU-901)
536
+ */
537
+ function handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args) {
538
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
539
+ if (!existsSync(WU_PATH))
540
+ return;
541
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
542
+ const wuContent = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
543
+ const wuDoc = parseYAML(wuContent);
544
+ const codePaths = wuDoc.code_paths || [];
545
+ if (codePaths.length === 0)
546
+ return;
547
+ const overlapCheck = detectConflicts(STATUS_PATH, codePaths, id);
548
+ emitWUFlowEvent({
549
+ script: 'wu-claim',
550
+ wu_id: id,
551
+ step: 'overlap_check',
552
+ conflicts_count: overlapCheck.conflicts.length,
553
+ forced: args.forceOverlap || false,
554
+ });
555
+ if (overlapCheck.hasBlocker && !args.forceOverlap) {
556
+ const conflictList = overlapCheck.conflicts
557
+ .map((c) => ` - ${c.wuid}: ${c.overlaps.slice(0, 3).join(', ')}${c.overlaps.length > 3 ? ` (+${c.overlaps.length - 3} more)` : ''}`)
558
+ .join(STRING_LITERALS.NEWLINE);
559
+ die(`Code path overlap detected with in-progress WUs:\n\n${conflictList}\n\n` +
560
+ `Merge conflicts are guaranteed if both WUs proceed.\n\n` +
561
+ `Options:\n` +
562
+ ` 1. Wait for conflicting WU(s) to complete\n` +
563
+ ` 2. Coordinate with agent working on conflicting WU\n` +
564
+ ` 3. Use --force-overlap --reason "..." (emits telemetry for audit)\n\n` +
565
+ `To check WU status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
566
+ }
567
+ if (args.forceOverlap) {
568
+ if (!args.reason) {
569
+ die('--force-overlap requires --reason "explanation" for audit trail');
570
+ }
571
+ emitWUFlowEvent({
572
+ script: 'wu-claim',
573
+ wu_id: id,
574
+ event: 'overlap_forced',
575
+ reason: args.reason,
576
+ conflicts: overlapCheck.conflicts.map((c) => ({ wuid: c.wuid, files: c.overlaps })),
577
+ });
578
+ console.warn(`${PREFIX} ⚠️ WARNING: Overlap forced with reason: ${args.reason}`);
579
+ }
580
+ }
581
+ /**
582
+ * Validate branch-only mode can be used
583
+ */
584
+ async function validateBranchOnlyMode(STATUS_PATH, id) {
585
+ const branchOnlyCheck = await checkExistingBranchOnlyWU(STATUS_PATH, id);
586
+ if (branchOnlyCheck.hasBranchOnly) {
587
+ die(`Branch-Only mode does not support parallel WUs.\n\n` +
588
+ `Another Branch-Only WU is already in progress: ${branchOnlyCheck.existingWU}\n\n` +
589
+ `Options:\n` +
590
+ ` 1. Complete ${branchOnlyCheck.existingWU} first (pnpm wu:done --id ${branchOnlyCheck.existingWU})\n` +
591
+ ` 2. Block ${branchOnlyCheck.existingWU} (pnpm wu:block --id ${branchOnlyCheck.existingWU} --reason "...")\n` +
592
+ ` 3. Use Worktree mode instead (omit --branch-only flag)\n\n` +
593
+ `Branch-Only mode works in the main checkout and cannot isolate parallel WUs.`);
594
+ }
595
+ // Ensure working directory is clean for Branch-Only mode
596
+ const status = await getGitForCwd().getStatus();
597
+ if (status) {
598
+ die(`Branch-Only mode requires a clean working directory.\n\n` +
599
+ `Uncommitted changes detected:\n${status}\n\n` +
600
+ `Options:\n` +
601
+ ` 1. Commit or stash your changes\n` +
602
+ ` 2. Use Worktree mode instead (omit --branch-only flag for isolated workspace)`);
603
+ }
604
+ }
605
+ /**
606
+ * Execute branch-only mode claim workflow
607
+ */
608
+ async function claimBranchOnlyMode(ctx) {
609
+ const { args, id, laneK, title, branch, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode } = ctx;
610
+ // WU-1438: Start agent session BEFORE metadata update to include session_id in YAML
611
+ let sessionId = null;
612
+ try {
613
+ const sessionResult = await startSessionForWU({
614
+ wuId: id,
615
+ tier: 2,
616
+ });
617
+ sessionId = sessionResult.sessionId;
618
+ if (sessionResult.alreadyActive) {
619
+ console.log(`${PREFIX} Agent session already active (${sessionId.slice(0, 8)}...)`);
620
+ }
621
+ else {
622
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Agent session started (${sessionId.slice(0, 8)}...)`);
623
+ }
624
+ }
625
+ catch (err) {
626
+ // Non-blocking: session start failure should not block claim
627
+ console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
628
+ }
629
+ // Create branch and switch to it (LEGACY - for constrained environments only)
630
+ await getGitForCwd().createBranch(branch, BRANCHES.MAIN);
631
+ // Update metadata in branch-only mode (on main checkout)
632
+ let updatedTitle = title;
633
+ if (args.noAuto) {
634
+ await ensureCleanOrClaimOnlyWhenNoAuto();
635
+ }
636
+ else {
637
+ updatedTitle =
638
+ (await updateWUYaml(WU_PATH, id, args.lane, claimedMode, null, sessionId)) || title;
639
+ await addOrReplaceInProgressStatus(STATUS_PATH, id, updatedTitle);
640
+ await removeFromReadyAndAddToInProgressBacklog(BACKLOG_PATH, id, updatedTitle, args.lane);
641
+ await getGitForCwd().add(`${JSON.stringify(WU_PATH)} ${JSON.stringify(STATUS_PATH)} ${JSON.stringify(BACKLOG_PATH)}`);
642
+ }
643
+ // Commit and push
644
+ const msg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
645
+ await getGitForCwd().commit(msg);
646
+ await getGitForCwd().push(REMOTES.ORIGIN, branch);
647
+ // Summary
648
+ console.log(`\n${PREFIX} Claim recorded in Branch-Only mode.`);
649
+ console.log(`- WU: ${id}${updatedTitle ? ` — ${updatedTitle}` : ''}`);
650
+ console.log(`- Lane: ${args.lane}`);
651
+ console.log(`- Mode: Branch-Only (no worktree)`);
652
+ console.log(`- Commit: ${msg}`);
653
+ console.log(`- Branch: ${branch}`);
654
+ console.log('\n⚠️ LIMITATION: Branch-Only mode does not support parallel WUs (WIP=1 across ALL lanes)');
655
+ console.log('Next: work on this branch in the main checkout.');
656
+ // WU-1360: Print next-steps checklist to prevent common mistakes
657
+ console.log(`\n${PREFIX} Next steps:`);
658
+ console.log(` 1. Work on this branch in the main checkout`);
659
+ console.log(` 2. Implement changes per acceptance criteria`);
660
+ console.log(` 3. Run: pnpm gates`);
661
+ console.log(` 4. pnpm wu:done --id ${id}`);
662
+ console.log(`\n${PREFIX} Common mistakes to avoid:`);
663
+ console.log(` - Don't manually edit WU YAML status fields`);
664
+ console.log(` - Don't create PRs (trunk-based development)`);
665
+ // WU-1501: Hint for sub-agent execution context
666
+ console.log(`\n${PREFIX} For sub-agent execution:`);
667
+ console.log(` /wu-prompt ${id} (generates full context prompt)`);
668
+ // Emit mandatory agent advisory based on code_paths (WU-1324)
669
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
670
+ const wuContent = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
671
+ const wuDoc = parseYAML(wuContent);
672
+ const codePaths = wuDoc.code_paths || [];
673
+ emitMandatoryAgentAdvisory(codePaths, id);
674
+ // WU-1763: Print lifecycle nudge with tips for tool adoption
675
+ printLifecycleNudge(id);
676
+ }
677
+ /**
678
+ * Execute worktree mode claim workflow
679
+ *
680
+ * WU-1741: Removed micro-worktree pattern that committed to main during claim.
681
+ * Branch existence (e.g. lane/operations/wu-1234) is the coordination lock.
682
+ * Metadata updates happen IN the work worktree, NOT on main.
683
+ *
684
+ * New flow:
685
+ * 1. Create work worktree+branch from main (branch = lock)
686
+ * 2. Update metadata (WU YAML, status.md, backlog.md) IN worktree
687
+ * 3. Commit metadata in worktree
688
+ * 4. Main only changes via wu:done (single merge point)
689
+ *
690
+ * Benefits:
691
+ * - Simpler mental model: main ONLY changes via wu:done
692
+ * - Branch existence is natural coordination (git prevents duplicates)
693
+ * - Less network traffic (no push during claim)
694
+ * - Cleaner rollback: delete worktree+branch = claim undone
695
+ */
696
+ async function claimWorktreeMode(ctx) {
697
+ const { args, id, laneK, title, branch, worktree, WU_PATH, BACKLOG_PATH, claimedMode, fixableIssues, // Fixable issues from pre-flight validation
698
+ } = ctx;
699
+ const originalCwd = process.cwd();
700
+ const worktreePath = path.resolve(worktree);
701
+ let updatedTitle = title;
702
+ const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
703
+ // WU-1438: Start agent session BEFORE metadata update to include session_id in YAML
704
+ let sessionId = null;
705
+ try {
706
+ const sessionResult = await startSessionForWU({
707
+ wuId: id,
708
+ tier: 2,
709
+ });
710
+ sessionId = sessionResult.sessionId;
711
+ if (sessionResult.alreadyActive) {
712
+ console.log(`${PREFIX} Agent session already active (${sessionId.slice(0, 8)}...)`);
713
+ }
714
+ else {
715
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Agent session started (${sessionId.slice(0, 8)}...)`);
716
+ }
717
+ }
718
+ catch (err) {
719
+ // Non-blocking: session start failure should not block claim
720
+ console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
721
+ }
722
+ // WU-1741: Step 1 - Create work worktree+branch from main
723
+ // Branch creation IS the coordination lock (git prevents duplicate branch names)
724
+ console.log(`${PREFIX} Creating worktree (branch = coordination lock)...`);
725
+ await getGitForCwd().worktreeAdd(worktree, branch, BRANCHES.MAIN);
726
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Worktree created at ${worktree}`);
727
+ // WU-1741: Step 2 - Update metadata IN the work worktree (not main)
728
+ if (!args.noAuto) {
729
+ // Build paths relative to work worktree
730
+ const wtWUPath = path.join(worktreePath, WU_PATH);
731
+ const wtBacklogPath = path.join(worktreePath, BACKLOG_PATH);
732
+ // Apply YAML fixes in worktree (not on main)
733
+ if (fixableIssues && fixableIssues.length > 0) {
734
+ console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
735
+ autoFixWUYaml(wtWUPath);
736
+ console.log(`${PREFIX} YAML fixes applied successfully`);
737
+ }
738
+ // Update metadata files in worktree (WU-1438: include session_id)
739
+ updatedTitle =
740
+ (await updateWUYaml(wtWUPath, id, args.lane, claimedMode, worktree, sessionId)) || title;
741
+ // WU-1746: Only append claim event to state store - don't regenerate backlog.md/status.md
742
+ // These generated files cause merge conflicts when committed to worktrees
743
+ const wtStateDir = getStateStoreDirFromBacklog(wtBacklogPath);
744
+ await appendClaimEventOnly(wtStateDir, id, updatedTitle, args.lane);
745
+ // WU-1741: Step 3 - Commit metadata in worktree (NOT on main)
746
+ // This commit stays on the lane branch until wu:done merges to main
747
+ console.log(`${PREFIX} Committing claim metadata in worktree...`);
748
+ const wtGit = createGitForPath(worktreePath);
749
+ // WU-1746: Use getWorktreeCommitFiles which excludes backlog.md and status.md
750
+ const filesToCommit = getWorktreeCommitFiles(id);
751
+ await wtGit.add(filesToCommit);
752
+ await wtGit.commit(commitMsg);
753
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Claim committed: ${commitMsg}`);
754
+ }
755
+ // WU-1443: Auto-symlink node_modules for immediate pnpm usability
756
+ // WU-2238: Pass mainRepoPath to detect broken worktree-path symlinks
757
+ const symlinkResult = symlinkNodeModules(worktreePath, console, originalCwd);
758
+ if (symlinkResult.created) {
759
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked for immediate use`);
760
+ }
761
+ else if (symlinkResult.refused) {
762
+ // WU-2238: Symlinking was refused due to worktree-path symlinks
763
+ // Fall back to running pnpm install in the worktree
764
+ console.log(`${PREFIX} Running pnpm install in worktree (symlink refused: ${symlinkResult.reason})`);
765
+ try {
766
+ const { execSync } = await import('node:child_process');
767
+ execSync('pnpm install --frozen-lockfile', {
768
+ cwd: worktreePath,
769
+ stdio: 'inherit',
770
+ timeout: 120000, // 2 minute timeout
771
+ });
772
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} pnpm install completed in worktree`);
773
+ }
774
+ catch (installError) {
775
+ console.warn(`${PREFIX} Warning: pnpm install failed: ${installError.message}`);
776
+ console.warn(`${PREFIX} You may need to run 'pnpm install' manually in the worktree`);
777
+ }
778
+ }
779
+ // WU-1579: Auto-symlink nested package node_modules for turbo typecheck
780
+ // WU-2238: Skip nested symlinks if root symlink was refused (pnpm install handles them)
781
+ if (!symlinkResult.refused) {
782
+ const nestedResult = symlinkNestedNodeModules(worktreePath, originalCwd);
783
+ if (nestedResult.created > 0) {
784
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
785
+ }
786
+ }
787
+ console.log(`${PREFIX} Claim recorded in worktree`);
788
+ console.log(`- WU: ${id}${updatedTitle ? ` — ${updatedTitle}` : ''}`);
789
+ console.log(`- Lane: ${args.lane}`);
790
+ console.log(`- Worktree: ${worktreePath}`);
791
+ console.log(`- Branch: ${branch}`);
792
+ console.log(`- Commit: ${commitMsg}`);
793
+ // Summary
794
+ console.log(`\n${PREFIX} Worktree created and claim committed.`);
795
+ console.log(`Next: cd ${worktree} and begin work.`);
796
+ // WU-1360: Print next-steps checklist to prevent common mistakes
797
+ console.log(`\n${PREFIX} Next steps:`);
798
+ console.log(` 1. cd ${worktree} (IMPORTANT: work here, not main)`);
799
+ console.log(` 2. Implement changes per acceptance criteria`);
800
+ console.log(` 3. Run: pnpm gates`);
801
+ console.log(` 4. cd ${originalCwd} && pnpm wu:done --id ${id}`);
802
+ console.log(`\n${PREFIX} Common mistakes to avoid:`);
803
+ console.log(` - Don't edit files on main branch`);
804
+ console.log(` - Don't manually edit WU YAML status fields`);
805
+ console.log(` - Don't create PRs (trunk-based development)`);
806
+ // WU-1501: Hint for sub-agent execution context
807
+ console.log(`\n${PREFIX} For sub-agent execution:`);
808
+ console.log(` /wu-prompt ${id} (generates full context prompt)`);
809
+ // Emit mandatory agent advisory based on code_paths (WU-1324)
810
+ // Read from worktree since that's where the updated YAML is
811
+ const wtWUPathForAdvisory = path.join(worktreePath, WU_PATH);
812
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
813
+ const wuContent = await readFile(wtWUPathForAdvisory, {
814
+ encoding: FILE_SYSTEM.UTF8,
815
+ });
816
+ const wuDoc = parseYAML(wuContent);
817
+ const codePaths = wuDoc.code_paths || [];
818
+ emitMandatoryAgentAdvisory(codePaths, id);
819
+ // WU-1763: Print lifecycle nudge with tips for tool adoption
820
+ printLifecycleNudge(id);
821
+ }
822
+ /**
823
+ * WU-1763: Print a single concise tips line to improve tool adoption.
824
+ * Non-blocking, single-line output to avoid flooding the console.
825
+ *
826
+ * @param {string} _id - WU ID being claimed (unused, kept for future use)
827
+ */
828
+ export function printLifecycleNudge(_id) {
829
+ // Single line, concise, actionable
830
+ console.log(`\n${PREFIX} 💡 Tip: pnpm session:recommend for context tier, mem:ready for pending work, pnpm file:*/git:* for audited wrappers`);
831
+ }
832
+ /**
833
+ * WU-2411: Handle --resume flag for agent handoff
834
+ *
835
+ * When an agent crashes or is killed, the --resume flag allows a new agent
836
+ * to take over by:
837
+ * 1. Verifying the old PID is dead (safety check)
838
+ * 2. Updating the lock file with the new PID
839
+ * 3. Preserving the existing worktree
840
+ * 4. Printing uncommitted changes summary
841
+ * 5. Creating a checkpoint in the memory layer
842
+ *
843
+ * @param {Object} args - CLI arguments
844
+ * @param {string} id - WU ID
845
+ */
846
+ async function handleResumeMode(args, id) {
847
+ const laneK = toKebab(args.lane);
848
+ const idK = id.toLowerCase();
849
+ const worktree = args.worktree || `worktrees/${laneK}-${idK}`;
850
+ const worktreePath = path.resolve(worktree);
851
+ console.log(`${PREFIX} Attempting to resume ${id} in lane "${args.lane}"...`);
852
+ // Attempt the resume/handoff
853
+ const result = await resumeClaimForHandoff({
854
+ wuId: id,
855
+ lane: args.lane,
856
+ worktreePath,
857
+ agentSession: null, // Will be populated by session system
858
+ });
859
+ if (!result.success) {
860
+ die(`Cannot resume ${id}: ${result.error}\n\n` +
861
+ `If you need to start a fresh claim, use: pnpm wu:claim --id ${id} --lane "${args.lane}"`);
862
+ }
863
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Handoff successful`);
864
+ console.log(`${PREFIX} Previous PID: ${result.previousPid}`);
865
+ console.log(`${PREFIX} New PID: ${process.pid}`);
866
+ // Get and display uncommitted changes in the worktree
867
+ const wtGit = createGitForPath(worktreePath);
868
+ const uncommittedStatus = await getWorktreeUncommittedChanges(wtGit);
869
+ if (uncommittedStatus) {
870
+ const formatted = formatUncommittedChanges(uncommittedStatus);
871
+ console.log(`\n${PREFIX} ${formatted}`);
872
+ }
873
+ else {
874
+ console.log(`\n${PREFIX} No uncommitted changes in worktree.`);
875
+ }
876
+ // Create handoff checkpoint in memory layer
877
+ const checkpointResult = await createHandoffCheckpoint({
878
+ wuId: id,
879
+ previousPid: result.previousPid,
880
+ newPid: process.pid,
881
+ previousSession: result.previousSession,
882
+ uncommittedSummary: uncommittedStatus,
883
+ });
884
+ if (checkpointResult.success && checkpointResult.checkpointId) {
885
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Handoff checkpoint created: ${checkpointResult.checkpointId}`);
886
+ }
887
+ // Emit telemetry event for handoff
888
+ emitWUFlowEvent({
889
+ script: 'wu-claim',
890
+ wu_id: id,
891
+ lane: args.lane,
892
+ step: 'resume_handoff',
893
+ previousPid: result.previousPid,
894
+ newPid: process.pid,
895
+ uncommittedChanges: uncommittedStatus ? 'present' : 'none',
896
+ });
897
+ // Print summary
898
+ console.log(`\n${PREFIX} Resume complete. Worktree preserved at: ${worktree}`);
899
+ console.log(`${PREFIX} Next: cd ${worktree} and continue work.`);
900
+ console.log(`\n${PREFIX} Tip: Run 'pnpm mem:ready --wu ${id}' to check for pending context from previous session.`);
901
+ }
902
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- main() orchestrates multi-step claim workflow
903
+ async function main() {
904
+ const args = createWUParser({
905
+ name: 'wu-claim',
906
+ description: 'Claim a work unit by creating a worktree/branch and updating status',
907
+ options: [
908
+ WU_OPTIONS.id,
909
+ WU_OPTIONS.lane,
910
+ WU_OPTIONS.worktree,
911
+ WU_OPTIONS.branch,
912
+ WU_OPTIONS.branchOnly,
913
+ WU_OPTIONS.prMode,
914
+ WU_OPTIONS.noAuto,
915
+ WU_OPTIONS.force,
916
+ WU_OPTIONS.forceOverlap,
917
+ WU_OPTIONS.fix,
918
+ WU_OPTIONS.reason,
919
+ WU_OPTIONS.allowIncomplete,
920
+ WU_OPTIONS.resume, // WU-2411: Agent handoff flag
921
+ ],
922
+ required: ['id', 'lane'],
923
+ allowPositionalId: true,
924
+ });
925
+ const id = args.id.toUpperCase();
926
+ if (!PATTERNS.WU_ID.test(id))
927
+ die(`Invalid WU id '${args.id}'. Expected format WU-123`);
928
+ await ensureOnMain(getGitForCwd());
929
+ // WU-2411: Handle --resume flag for agent handoff
930
+ if (args.resume) {
931
+ await handleResumeMode(args, id);
932
+ return; // Resume mode handles its own flow
933
+ }
934
+ // Preflight: ensure working tree is clean (unless --no-auto, which expects staged changes)
935
+ if (!args.noAuto) {
936
+ const status = await getGitForCwd().getStatus();
937
+ if (status.trim()) {
938
+ die(`Working tree is not clean. Commit or stash changes before claiming.\n\n` +
939
+ `Uncommitted changes:\n${status}\n\n` +
940
+ `Options:\n` +
941
+ ` 1. git add . && git commit -m "..."\n` +
942
+ ` 2. git stash\n` +
943
+ ` 3. Use --no-auto if you already staged claim edits manually`);
944
+ }
945
+ }
946
+ // WU-1361: Fetch and pull FIRST - validate on fresh data, not stale pre-pull data
947
+ await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
948
+ await getGitForCwd().pull(REMOTES.ORIGIN, BRANCHES.MAIN);
949
+ const WU_PATH = WU_PATHS.WU(id);
950
+ const STATUS_PATH = WU_PATHS.STATUS();
951
+ const BACKLOG_PATH = WU_PATHS.BACKLOG();
952
+ // PRE-FLIGHT VALIDATION (on post-pull data)
953
+ const doc = preflightValidateWU(WU_PATH, id);
954
+ await handleOrphanCheck(args.lane, id);
955
+ validateLaneFormatWithError(args.lane);
956
+ // WU-1372: Lane-to-code_paths consistency check (advisory only, never blocks)
957
+ const laneValidation = validateLaneCodePaths(doc, args.lane);
958
+ logLaneValidationWarnings(laneValidation, PREFIX);
959
+ // WU-1361: YAML schema validation at claim time
960
+ // Returns fixable issues for application in worktree (not on main)
961
+ const fixableIssues = validateYAMLSchema(WU_PATH, doc, args);
962
+ // WU-1506/WU-1576: Backlog invariant repair moved inside micro-worktree (see claimWorktreeMode)
963
+ // Previously called validateBacklogConsistency(BACKLOG_PATH) here which modified main directly
964
+ // WU-1362: Spec completeness validation (fail-fast before expensive operations)
965
+ // Two-tier validation: Schema errors (above) are never bypassable; spec completeness is bypassable
966
+ const specResult = validateSpecCompleteness(doc, id);
967
+ if (!specResult.valid) {
968
+ const errorList = specResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE);
969
+ if (args.allowIncomplete) {
970
+ console.warn(`${PREFIX} ⚠️ Spec completeness warnings (bypassed with --allow-incomplete):`);
971
+ console.warn(errorList);
972
+ console.warn(`${PREFIX} Proceeding with incomplete spec. Fix before wu:done.`);
973
+ }
974
+ else {
975
+ die(`Spec completeness validation failed for ${WU_PATH}:\n\n${errorList}\n\n` +
976
+ `Fix these issues before claiming, or use --allow-incomplete to bypass.\n` +
977
+ `Note: Schema errors (placeholders, invalid structure) cannot be bypassed.`);
978
+ }
979
+ }
980
+ // Check lane occupancy (WIP=1 per sub-lane)
981
+ const laneCheck = checkLaneFree(STATUS_PATH, args.lane, id);
982
+ emitWUFlowEvent({
983
+ script: 'wu-claim',
984
+ wu_id: id,
985
+ lane: args.lane,
986
+ step: 'lane_check',
987
+ occupied: !laneCheck.free,
988
+ occupiedBy: laneCheck.occupiedBy,
989
+ });
990
+ handleLaneOccupancy(laneCheck, args.lane, id, args.force);
991
+ // WU-1603: Atomic lane lock to prevent TOCTOU race conditions
992
+ // This is Layer 2 defense after status.md check - prevents parallel agents from
993
+ // both reading a free status.md before either updates it
994
+ const existingLock = checkLaneLock(args.lane);
995
+ if (existingLock.locked && existingLock.isStale) {
996
+ console.log(`${PREFIX} Detected stale lock for "${args.lane}" (${existingLock.metadata.wuId})`);
997
+ console.log(`${PREFIX} Lock timestamp: ${existingLock.metadata.timestamp}`);
998
+ forceRemoveStaleLock(args.lane);
999
+ }
1000
+ const lockResult = acquireLaneLock(args.lane, id, {
1001
+ agentSession: null, // Will be set after session starts
1002
+ });
1003
+ if (!lockResult.acquired) {
1004
+ // Lock acquisition failed - another agent got there first
1005
+ const staleSuffix = lockResult.isStale
1006
+ ? '\n\nNote: This lock may be stale (>24h). Use --force to override if the owning WU is abandoned.'
1007
+ : '';
1008
+ die(`Cannot claim ${id}: ${lockResult.error}\n\n` +
1009
+ `Another agent is actively claiming or has claimed this lane.\n\n` +
1010
+ `Options:\n` +
1011
+ ` 1. Wait for ${lockResult.existingLock?.wuId || 'the other WU'} to complete or block\n` +
1012
+ ` 2. Choose a different lane\n` +
1013
+ ` 3. Use --force to override (P0 emergencies only)${staleSuffix}`);
1014
+ }
1015
+ emitWUFlowEvent({
1016
+ script: 'wu-claim',
1017
+ wu_id: id,
1018
+ lane: args.lane,
1019
+ step: 'lane_lock_acquired',
1020
+ });
1021
+ // WU-1808: Wrap claim execution in try/finally to ensure lock release on failure
1022
+ // If claim fails after lock acquisition, the lane would be blocked without this cleanup
1023
+ let claimSucceeded = false;
1024
+ try {
1025
+ // Code paths overlap detection (WU-901)
1026
+ handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args);
1027
+ // Prepare paths and branches
1028
+ const laneK = toKebab(args.lane);
1029
+ const idK = id.toLowerCase();
1030
+ const title = (await readWUTitle(id)) || '';
1031
+ const branch = args.branch || `lane/${laneK}/${idK}`;
1032
+ const worktree = args.worktree || `worktrees/${laneK}-${idK}`;
1033
+ const claimedMode = args.branchOnly
1034
+ ? CLAIMED_MODES.BRANCH_ONLY
1035
+ : args.prMode
1036
+ ? CLAIMED_MODES.WORKTREE_PR
1037
+ : CLAIMED_MODES.WORKTREE;
1038
+ // Branch-Only mode validation
1039
+ if (args.branchOnly) {
1040
+ await validateBranchOnlyMode(STATUS_PATH, id);
1041
+ }
1042
+ // Check if branch already exists (prevents duplicate claims)
1043
+ const branchAlreadyExists = await getGitForCwd().branchExists(branch);
1044
+ if (branchAlreadyExists) {
1045
+ die(`Branch ${branch} already exists. WU may already be claimed.\n\n` +
1046
+ `Git branch existence = WU claimed (natural locking).\n\n` +
1047
+ `Options:\n` +
1048
+ ` 1. Check git worktree list to see if worktree exists\n` +
1049
+ ` 2. Coordinate with the owning agent or wait for them to complete\n` +
1050
+ ` 3. Choose a different WU`);
1051
+ }
1052
+ // Layer 3 defense (WU-1476): Pre-flight orphan check
1053
+ // Clean up orphan directory if it exists at target worktree path
1054
+ const absoluteWorktreePath = path.resolve(worktree);
1055
+ if (await isOrphanWorktree(absoluteWorktreePath, process.cwd())) {
1056
+ console.log(`${PREFIX} Detected orphan directory at ${worktree}, cleaning up...`);
1057
+ try {
1058
+ rmSync(absoluteWorktreePath, { recursive: true, force: true });
1059
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Orphan directory removed`);
1060
+ }
1061
+ catch (err) {
1062
+ die(`Failed to clean up orphan directory at ${worktree}\n\n` +
1063
+ `Error: ${err.message}\n\n` +
1064
+ `Manual cleanup: rm -rf ${absoluteWorktreePath}`);
1065
+ }
1066
+ }
1067
+ // Execute claim workflow
1068
+ const ctx = {
1069
+ args,
1070
+ id,
1071
+ laneK,
1072
+ title,
1073
+ branch,
1074
+ worktree,
1075
+ WU_PATH,
1076
+ STATUS_PATH,
1077
+ BACKLOG_PATH,
1078
+ claimedMode,
1079
+ fixableIssues, // WU-1361: Pass fixable issues for worktree application
1080
+ };
1081
+ if (args.branchOnly) {
1082
+ await claimBranchOnlyMode(ctx);
1083
+ }
1084
+ else {
1085
+ await claimWorktreeMode(ctx);
1086
+ }
1087
+ // Mark claim as successful - lock should remain for wu:done to release
1088
+ claimSucceeded = true;
1089
+ }
1090
+ finally {
1091
+ // WU-1808: Release lane lock if claim did not complete successfully
1092
+ // This prevents orphan locks from blocking the lane when claim crashes or fails
1093
+ if (!claimSucceeded) {
1094
+ console.log(`${PREFIX} Claim did not complete - releasing lane lock...`);
1095
+ const releaseResult = releaseLaneLock(args.lane, { wuId: id });
1096
+ if (releaseResult.released && !releaseResult.notFound) {
1097
+ console.log(`${PREFIX} Lane lock released for "${args.lane}"`);
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ // Guard main() for testability (WU-1366)
1103
+ import { fileURLToPath } from 'node:url';
1104
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
1105
+ main();
1106
+ }