@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.
- package/LICENSE +190 -0
- package/README.md +116 -0
- package/dist/gates.d.ts +41 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +684 -0
- package/dist/gates.js.map +1 -0
- package/dist/initiative-add-wu.d.ts +22 -0
- package/dist/initiative-add-wu.d.ts.map +1 -0
- package/dist/initiative-add-wu.js +234 -0
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-create.d.ts +28 -0
- package/dist/initiative-create.d.ts.map +1 -0
- package/dist/initiative-create.js +172 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.d.ts +34 -0
- package/dist/initiative-edit.d.ts.map +1 -0
- package/dist/initiative-edit.js +440 -0
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.d.ts +12 -0
- package/dist/initiative-list.d.ts.map +1 -0
- package/dist/initiative-list.js +101 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-status.d.ts +11 -0
- package/dist/initiative-status.d.ts.map +1 -0
- package/dist/initiative-status.js +221 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/mem-checkpoint.d.ts +16 -0
- package/dist/mem-checkpoint.d.ts.map +1 -0
- package/dist/mem-checkpoint.js +237 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.d.ts +29 -0
- package/dist/mem-cleanup.d.ts.map +1 -0
- package/dist/mem-cleanup.js +267 -0
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-create.d.ts +17 -0
- package/dist/mem-create.d.ts.map +1 -0
- package/dist/mem-create.js +265 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-inbox.d.ts +35 -0
- package/dist/mem-inbox.d.ts.map +1 -0
- package/dist/mem-inbox.js +373 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-init.d.ts +15 -0
- package/dist/mem-init.d.ts.map +1 -0
- package/dist/mem-init.js +146 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-ready.d.ts +16 -0
- package/dist/mem-ready.d.ts.map +1 -0
- package/dist/mem-ready.js +224 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-signal.d.ts +16 -0
- package/dist/mem-signal.d.ts.map +1 -0
- package/dist/mem-signal.js +204 -0
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.d.ts +16 -0
- package/dist/mem-start.d.ts.map +1 -0
- package/dist/mem-start.js +158 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.d.ts +22 -0
- package/dist/mem-summarize.d.ts.map +1 -0
- package/dist/mem-summarize.js +213 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.d.ts +22 -0
- package/dist/mem-triage.d.ts.map +1 -0
- package/dist/mem-triage.js +328 -0
- package/dist/mem-triage.js.map +1 -0
- package/dist/spawn-list.d.ts +16 -0
- package/dist/spawn-list.d.ts.map +1 -0
- package/dist/spawn-list.js +140 -0
- package/dist/spawn-list.js.map +1 -0
- package/dist/wu-block.d.ts +16 -0
- package/dist/wu-block.d.ts.map +1 -0
- package/dist/wu-block.js +241 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-claim.d.ts +32 -0
- package/dist/wu-claim.d.ts.map +1 -0
- package/dist/wu-claim.js +1106 -0
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup.d.ts +17 -0
- package/dist/wu-cleanup.d.ts.map +1 -0
- package/dist/wu-cleanup.js +194 -0
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-create.d.ts +38 -0
- package/dist/wu-create.d.ts.map +1 -0
- package/dist/wu-create.js +520 -0
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-deps.d.ts +13 -0
- package/dist/wu-deps.d.ts.map +1 -0
- package/dist/wu-deps.js +119 -0
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done.d.ts +153 -0
- package/dist/wu-done.d.ts.map +1 -0
- package/dist/wu-done.js +2096 -0
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.d.ts +29 -0
- package/dist/wu-edit.d.ts.map +1 -0
- package/dist/wu-edit.js +852 -0
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.d.ts +17 -0
- package/dist/wu-infer-lane.d.ts.map +1 -0
- package/dist/wu-infer-lane.js +135 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.d.ts +47 -0
- package/dist/wu-preflight.d.ts.map +1 -0
- package/dist/wu-preflight.js +167 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prune.d.ts +16 -0
- package/dist/wu-prune.d.ts.map +1 -0
- package/dist/wu-prune.js +259 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-repair.d.ts +60 -0
- package/dist/wu-repair.d.ts.map +1 -0
- package/dist/wu-repair.js +226 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-spawn-completion.d.ts +10 -0
- package/dist/wu-spawn-completion.js +30 -0
- package/dist/wu-spawn.d.ts +168 -0
- package/dist/wu-spawn.d.ts.map +1 -0
- package/dist/wu-spawn.js +1327 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-unblock.d.ts +16 -0
- package/dist/wu-unblock.d.ts.map +1 -0
- package/dist/wu-unblock.js +234 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-validate.d.ts +16 -0
- package/dist/wu-validate.d.ts.map +1 -0
- package/dist/wu-validate.js +193 -0
- package/dist/wu-validate.js.map +1 -0
- package/package.json +92 -0
package/dist/wu-claim.js
ADDED
|
@@ -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
|
+
}
|