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