@lumenflow/cli 2.18.3 → 2.20.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 +44 -42
- package/dist/agent-session.js +1 -1
- package/dist/agent-session.js.map +1 -1
- package/dist/commands/integrate.js +1 -0
- package/dist/commands/integrate.js.map +1 -1
- package/dist/commands.js +1 -0
- package/dist/commands.js.map +1 -1
- package/dist/delegation-list.js +140 -0
- package/dist/delegation-list.js.map +1 -0
- package/dist/docs-sync.js +1 -0
- package/dist/docs-sync.js.map +1 -1
- package/dist/doctor.js +36 -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 +6 -5
- 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 +137 -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-initiative.js +2 -2
- package/dist/orchestrate-initiative.js.map +1 -1
- package/dist/orchestrate-monitor.js +38 -38
- package/dist/orchestrate-monitor.js.map +1 -1
- package/dist/plan-link.js +7 -14
- package/dist/plan-link.js.map +1 -1
- package/dist/public-manifest.js +19 -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/sync-templates.js +2 -1
- package/dist/sync-templates.js.map +1 -1
- 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 +96 -1394
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-code-path-coverage.js +81 -0
- package/dist/wu-code-path-coverage.js.map +1 -0
- package/dist/wu-create-content.js +256 -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 +124 -0
- package/dist/wu-create-validation.js.map +1 -0
- package/dist/wu-create.js +45 -442
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +151 -249
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +401 -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 +43 -759
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-prep.js +43 -127
- 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-sandbox.js +253 -0
- package/dist/wu-sandbox.js.map +1 -0
- package/dist/wu-spawn-prompt-builders.js +1124 -0
- package/dist/wu-spawn-prompt-builders.js.map +1 -0
- package/dist/wu-spawn-strategy-resolver.js +319 -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/dist/wu-status.js +4 -0
- package/dist/wu-status.js.map +1 -1
- package/dist/wu-validate.js +1 -1
- package/dist/wu-validate.js.map +1 -1
- package/package.json +15 -11
- package/templates/core/LUMENFLOW.md.template +29 -99
- package/templates/core/UPGRADING.md.template +2 -2
- 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/core/ai/onboarding/release-process.md.template +1 -1
- 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,84 +39,43 @@ 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, EXIT_CODES, } 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
|
-
|
|
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
|
+
import { extractSandboxCommandFromArgv, runWuSandbox } from './wu-sandbox.js';
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// RE-EXPORTS: Preserve public API for existing test consumers
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// From wu-claim-validation.ts
|
|
62
|
+
export { resolveClaimStatus, validateManualTestsForClaim } from './wu-claim-validation.js';
|
|
63
|
+
// From wu-claim-state.ts
|
|
64
|
+
export { shouldApplyCanonicalClaimUpdate, shouldPersistClaimMetadataOnBranch, resolveClaimBaselineRef, buildRollbackYamlDoc, hasClaimPickupEvidence, recordClaimPickupEvidence, getWorktreeCommitFiles, } from './wu-claim-state.js';
|
|
65
|
+
// From wu-claim-output.ts
|
|
66
|
+
export { formatProjectDefaults, printProjectDefaults, printLifecycleNudge, } from './wu-claim-output.js';
|
|
67
|
+
// From wu-claim-worktree.ts
|
|
68
|
+
export { applyFallbackSymlinks } from './wu-claim-worktree.js';
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Cloud activation (kept in orchestrator since it's only used in main())
|
|
71
|
+
// ============================================================================
|
|
121
72
|
const PREFIX = LOG_PREFIX.CLAIM;
|
|
73
|
+
const WU_CLAIM_SANDBOX_OPTION = {
|
|
74
|
+
name: 'sandbox',
|
|
75
|
+
flags: '--sandbox',
|
|
76
|
+
description: 'Launch a post-claim session via wu:sandbox (use -- <command> to override the default shell)',
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
};
|
|
122
79
|
/**
|
|
123
80
|
* Resolve branch-aware cloud activation for wu:claim.
|
|
124
81
|
*
|
|
@@ -136,1270 +93,36 @@ export function resolveCloudActivationForClaim(input) {
|
|
|
136
93
|
currentBranch: input.currentBranch,
|
|
137
94
|
});
|
|
138
95
|
}
|
|
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 };
|
|
96
|
+
export function resolveDefaultClaimSandboxCommand(env = process.env, platform = process.platform) {
|
|
97
|
+
if (platform === 'win32') {
|
|
98
|
+
return ['powershell.exe', '-NoLogo'];
|
|
481
99
|
}
|
|
100
|
+
const shell = env.SHELL?.trim();
|
|
101
|
+
return shell && shell.length > 0 ? [shell] : ['/bin/sh'];
|
|
482
102
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
}
|
|
103
|
+
export function resolveClaimSandboxCommand(argv = process.argv, env = process.env, platform = process.platform) {
|
|
104
|
+
const explicit = extractSandboxCommandFromArgv(argv);
|
|
105
|
+
if (explicit.length > 0) {
|
|
106
|
+
return explicit;
|
|
516
107
|
}
|
|
517
|
-
|
|
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
|
-
];
|
|
108
|
+
return resolveDefaultClaimSandboxCommand(env, platform);
|
|
577
109
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
const status = parts[0];
|
|
581
|
-
if (!status)
|
|
110
|
+
export async function maybeLaunchClaimSandboxSession(input, deps = {}) {
|
|
111
|
+
if (!input.enabled) {
|
|
582
112
|
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
113
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
},
|
|
114
|
+
const launchSandbox = deps.launchSandbox || runWuSandbox;
|
|
115
|
+
const command = resolveClaimSandboxCommand(input.argv || process.argv, input.env || process.env, input.platform || process.platform);
|
|
116
|
+
console.log(`${PREFIX} Launching post-claim session via wu:sandbox...`);
|
|
117
|
+
return launchSandbox({
|
|
118
|
+
id: input.id,
|
|
119
|
+
worktree: input.worktreePath,
|
|
120
|
+
command,
|
|
670
121
|
});
|
|
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
122
|
}
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Main orchestrator
|
|
125
|
+
// ============================================================================
|
|
1403
126
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- main() orchestrates multi-step claim workflow
|
|
1404
127
|
async function main() {
|
|
1405
128
|
const args = createWUParser({
|
|
@@ -1422,6 +145,7 @@ async function main() {
|
|
|
1422
145
|
WU_OPTIONS.resume, // WU-2411: Agent handoff flag
|
|
1423
146
|
WU_OPTIONS.skipSetup, // WU-1023: Skip auto-setup for fast claims
|
|
1424
147
|
WU_OPTIONS.noPush, // Skip pushing claim state/branch (air-gapped)
|
|
148
|
+
WU_CLAIM_SANDBOX_OPTION,
|
|
1425
149
|
],
|
|
1426
150
|
required: ['id', 'lane'],
|
|
1427
151
|
allowPositionalId: true,
|
|
@@ -1480,64 +204,23 @@ async function main() {
|
|
|
1480
204
|
stagedChanges = await getStagedChanges();
|
|
1481
205
|
}
|
|
1482
206
|
// WU-1361: Fetch latest remote before validation (no local main mutation)
|
|
1483
|
-
|
|
207
|
+
// WU-1653: Also skip when git.requireRemote=false (local-only mode)
|
|
208
|
+
const skipRemote = shouldSkipRemoteOperations();
|
|
209
|
+
if (!args.noPush && !skipRemote) {
|
|
1484
210
|
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
1485
211
|
await ensureMainUpToDate(getGitForCwd(), 'wu:claim');
|
|
1486
212
|
}
|
|
213
|
+
else if (skipRemote) {
|
|
214
|
+
console.log(`${PREFIX} Local-only mode (git.requireRemote=false): skipping origin sync`);
|
|
215
|
+
}
|
|
1487
216
|
else {
|
|
1488
217
|
console.warn(`${PREFIX} Warning: --no-push enabled. Skipping origin/main sync; local state may be stale.`);
|
|
1489
218
|
}
|
|
1490
219
|
const WU_PATH = WU_PATHS.WU(id);
|
|
1491
220
|
const STATUS_PATH = WU_PATHS.STATUS();
|
|
1492
221
|
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);
|
|
222
|
+
// WU-1649: Delegated to wu-claim-validation.ts
|
|
223
|
+
const { fixableIssues } = await runPreflightValidations(args, id, WU_PATH, STATUS_PATH);
|
|
1541
224
|
// WU-1603: Atomic lane lock to prevent TOCTOU race conditions
|
|
1542
225
|
// This is Layer 2 defense after status.md check - prevents parallel agents from
|
|
1543
226
|
// both reading a free status.md before either updates it
|
|
@@ -1574,6 +257,7 @@ async function main() {
|
|
|
1574
257
|
// WU-1521: Track canonical claim push state for rollback in finally block
|
|
1575
258
|
let canonicalClaimPushed = false;
|
|
1576
259
|
let claimTitle = '';
|
|
260
|
+
let postClaimSandboxWorktree = null;
|
|
1577
261
|
try {
|
|
1578
262
|
// Code paths overlap detection (WU-901)
|
|
1579
263
|
handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args);
|
|
@@ -1617,7 +301,8 @@ async function main() {
|
|
|
1617
301
|
laneBranch: effectiveBranch,
|
|
1618
302
|
});
|
|
1619
303
|
// Check if remote branch already exists (prevents duplicate global claims)
|
|
1620
|
-
|
|
304
|
+
// WU-1653: Skip when requireRemote=false (no remote to check)
|
|
305
|
+
if (!args.noPush && !skipBranchChecks && !shouldSkipRemoteOperations()) {
|
|
1621
306
|
const remoteExists = await getGitForCwd().remoteBranchExists(REMOTES.ORIGIN, effectiveBranch);
|
|
1622
307
|
if (remoteExists) {
|
|
1623
308
|
die(`Remote branch ${REMOTES.ORIGIN}/${effectiveBranch} already exists. WU may already be claimed.\n\n` +
|
|
@@ -1693,7 +378,7 @@ async function main() {
|
|
|
1693
378
|
};
|
|
1694
379
|
let updatedTitle = title;
|
|
1695
380
|
claimTitle = title;
|
|
1696
|
-
const shouldApplyCanonicalUpdate =
|
|
381
|
+
const shouldApplyCanonicalUpdate = shouldApplyCanonicalClaimUpdateFn({
|
|
1697
382
|
isCloud: effectiveCloud,
|
|
1698
383
|
claimedMode,
|
|
1699
384
|
noPush: Boolean(args.noPush),
|
|
@@ -1705,7 +390,10 @@ async function main() {
|
|
|
1705
390
|
canonicalClaimPushed = true;
|
|
1706
391
|
claimTitle = updatedTitle || title;
|
|
1707
392
|
// Refresh origin/main after push-only update so worktrees start from canonical state
|
|
1708
|
-
|
|
393
|
+
// WU-1653: Skip fetch when requireRemote=false (no remote)
|
|
394
|
+
if (!shouldSkipRemoteOperations()) {
|
|
395
|
+
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
396
|
+
}
|
|
1709
397
|
}
|
|
1710
398
|
else if (!args.noPush && claimedMode === CLAIMED_MODES.BRANCH_PR) {
|
|
1711
399
|
console.log(`${PREFIX} Skipping canonical claim update on origin/main for cloud branch-pr claim.`);
|
|
@@ -1749,6 +437,10 @@ async function main() {
|
|
|
1749
437
|
}
|
|
1750
438
|
// Mark claim as successful - lock should remain for wu:done to release
|
|
1751
439
|
claimSucceeded = true;
|
|
440
|
+
postClaimSandboxWorktree =
|
|
441
|
+
claimedMode === CLAIMED_MODES.BRANCH_ONLY || claimedMode === CLAIMED_MODES.BRANCH_PR
|
|
442
|
+
? process.cwd()
|
|
443
|
+
: path.resolve(worktree);
|
|
1752
444
|
}
|
|
1753
445
|
finally {
|
|
1754
446
|
// WU-1808: Release lane lock if claim did not complete successfully
|
|
@@ -1766,6 +458,16 @@ async function main() {
|
|
|
1766
458
|
}
|
|
1767
459
|
}
|
|
1768
460
|
}
|
|
461
|
+
const sandboxExitCode = await maybeLaunchClaimSandboxSession({
|
|
462
|
+
enabled: Boolean(args.sandbox && claimSucceeded && postClaimSandboxWorktree),
|
|
463
|
+
id,
|
|
464
|
+
worktreePath: postClaimSandboxWorktree || process.cwd(),
|
|
465
|
+
argv: process.argv,
|
|
466
|
+
});
|
|
467
|
+
if (sandboxExitCode !== null && sandboxExitCode !== EXIT_CODES.SUCCESS) {
|
|
468
|
+
die(`Post-claim sandbox command exited with code ${sandboxExitCode}.\n` +
|
|
469
|
+
`Claim for ${id} remains active. Resume from ${postClaimSandboxWorktree || process.cwd()}.`);
|
|
470
|
+
}
|
|
1769
471
|
}
|
|
1770
472
|
// Guard main() for testability (WU-1366)
|
|
1771
473
|
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|