@lumenflow/cli 2.17.0 → 2.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +24 -23
  2. package/dist/gates.js +56 -6
  3. package/dist/gates.js.map +1 -1
  4. package/dist/hooks/enforcement-generator.js +3 -3
  5. package/dist/init.js +1 -1
  6. package/dist/orchestrate-initiative.js +4 -3
  7. package/dist/orchestrate-initiative.js.map +1 -1
  8. package/dist/orchestrate-monitor.js +2 -1
  9. package/dist/orchestrate-monitor.js.map +1 -1
  10. package/dist/public-manifest.js +11 -4
  11. package/dist/public-manifest.js.map +1 -1
  12. package/dist/spawn-list.js +1 -0
  13. package/dist/spawn-list.js.map +1 -1
  14. package/dist/wu-block.js +89 -45
  15. package/dist/wu-block.js.map +1 -1
  16. package/dist/wu-brief.js +40 -0
  17. package/dist/wu-brief.js.map +1 -0
  18. package/dist/wu-claim-cloud.js +61 -0
  19. package/dist/wu-claim-cloud.js.map +1 -0
  20. package/dist/wu-claim.js +234 -57
  21. package/dist/wu-claim.js.map +1 -1
  22. package/dist/wu-cleanup-cloud.js +76 -0
  23. package/dist/wu-cleanup-cloud.js.map +1 -0
  24. package/dist/wu-cleanup.js +42 -19
  25. package/dist/wu-cleanup.js.map +1 -1
  26. package/dist/wu-create-cloud.js +40 -0
  27. package/dist/wu-create-cloud.js.map +1 -0
  28. package/dist/wu-create.js +137 -53
  29. package/dist/wu-create.js.map +1 -1
  30. package/dist/wu-delegate.js +21 -0
  31. package/dist/wu-delegate.js.map +1 -0
  32. package/dist/wu-delete.js +102 -61
  33. package/dist/wu-delete.js.map +1 -1
  34. package/dist/wu-done-auto-cleanup.js +5 -16
  35. package/dist/wu-done-auto-cleanup.js.map +1 -1
  36. package/dist/wu-done-cloud.js +46 -0
  37. package/dist/wu-done-cloud.js.map +1 -0
  38. package/dist/wu-done.js +186 -44
  39. package/dist/wu-done.js.map +1 -1
  40. package/dist/wu-edit.js +217 -55
  41. package/dist/wu-edit.js.map +1 -1
  42. package/dist/wu-prep.js +1 -1
  43. package/dist/wu-prep.js.map +1 -1
  44. package/dist/wu-recover.js +128 -55
  45. package/dist/wu-recover.js.map +1 -1
  46. package/dist/wu-release.js +99 -45
  47. package/dist/wu-release.js.map +1 -1
  48. package/dist/wu-spawn.js +108 -42
  49. package/dist/wu-spawn.js.map +1 -1
  50. package/dist/wu-state-cloud.js +39 -0
  51. package/dist/wu-state-cloud.js.map +1 -0
  52. package/dist/wu-unblock.js +99 -49
  53. package/dist/wu-unblock.js.map +1 -1
  54. package/package.json +8 -7
  55. package/templates/core/.lumenflow/constraints.md.template +31 -4
  56. package/templates/core/LUMENFLOW.md.template +31 -9
  57. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +21 -13
  58. package/templates/core/ai/onboarding/agent-safety-card.md.template +2 -2
  59. package/templates/core/ai/onboarding/docs-generation.md.template +1 -1
  60. package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +1 -1
  61. package/templates/core/ai/onboarding/quick-ref-commands.md.template +235 -66
  62. package/templates/core/ai/onboarding/rapid-prototyping.md +2 -2
  63. package/templates/core/ai/onboarding/starting-prompt.md.template +124 -24
  64. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +11 -0
  65. package/templates/core/ai/onboarding/vendor-support.md.template +58 -69
  66. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +3 -3
  67. package/templates/vendors/claude/.claude/skills/design-first/SKILL.md.template +151 -0
  68. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +8 -8
  69. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +1 -0
  70. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +5 -5
  71. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +19 -16
  72. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +2 -0
package/dist/wu-claim.js CHANGED
@@ -33,14 +33,16 @@ import { validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/co
33
33
  // WU-1574: parseBacklogFrontmatter/getSectionHeadings removed - state store replaces backlog parsing
34
34
  import { detectConflicts } from '@lumenflow/core/code-paths-overlap';
35
35
  import { getGitForCwd, createGitForPath } from '@lumenflow/core/git-adapter';
36
- import { die } from '@lumenflow/core/error-handler';
36
+ import { die, getErrorMessage } from '@lumenflow/core/error-handler';
37
37
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
38
38
  // WU-1491: Mode resolution for --cloud and flag combinations
39
39
  import { resolveClaimMode } from './wu-claim-mode.js';
40
+ // WU-1590: Cloud claim helpers for branch-pr/cloud execution behavior
41
+ import { shouldSkipBranchExistsCheck, resolveBranchClaimExecution } from './wu-claim-cloud.js';
40
42
  // WU-1495: Cloud auto-detection from config-driven env signals
41
- import { detectCloudMode } from '@lumenflow/core/cloud-detect';
43
+ import { detectCloudMode, resolveEffectiveCloudActivation, CLOUD_ACTIVATION_SOURCE, } from '@lumenflow/core/cloud-detect';
42
44
  import { WU_PATHS, getStateStoreDirFromBacklog } from '@lumenflow/core/wu-paths';
43
- import { BRANCHES, REMOTES, WU_STATUS, CLAIMED_MODES, STATUS_SECTIONS, PATTERNS, toKebab, LOG_PREFIX, GIT_REFS, MICRO_WORKTREE_OPERATIONS, COMMIT_FORMATS, EMOJI, FILE_SYSTEM, STRING_LITERALS, LUMENFLOW_PATHS, } from '@lumenflow/core/wu-constants';
45
+ import { BRANCHES, REMOTES, WU_STATUS, CLAIMED_MODES, STATUS_SECTIONS, PATTERNS, toKebab, LOG_PREFIX, GIT_REFS, MICRO_WORKTREE_OPERATIONS, COMMIT_FORMATS, EMOJI, FILE_SYSTEM, STRING_LITERALS, LUMENFLOW_PATHS, resolveWUStatus, } from '@lumenflow/core/wu-constants';
44
46
  import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
45
47
  import { ensureOnMain, ensureMainUpToDate } from '@lumenflow/core/wu-helpers';
46
48
  import { emitWUFlowEvent } from '@lumenflow/core/telemetry';
@@ -58,6 +60,7 @@ import { getAssignedEmail } from '@lumenflow/core/wu-claim-helpers';
58
60
  import { symlinkNodeModules, symlinkNestedNodeModules } from '@lumenflow/core/worktree-symlink';
59
61
  // WU-1572: Import WUStateStore for event-sourced state tracking
60
62
  import { WUStateStore } from '@lumenflow/core/wu-state-store';
63
+ import { SpawnRegistryStore } from '@lumenflow/core/spawn-registry-store';
61
64
  // WU-1574: Import backlog generator to replace BacklogManager
62
65
  import { generateBacklog, generateStatus } from '@lumenflow/core/backlog-generator';
63
66
  // WU-2411: Import resume helpers for agent handoff
@@ -90,7 +93,7 @@ async function surfaceUnreadSignalsForDisplay(baseDir) {
90
93
  }
91
94
  catch (err) {
92
95
  // WU-1473 AC4: Fail-open - never block claim on memory errors
93
- console.warn(`${PREFIX} Warning: Could not surface unread signals: ${err.message}`);
96
+ console.warn(`${PREFIX} Warning: Could not surface unread signals: ${getErrorMessage(err)}`);
94
97
  }
95
98
  }
96
99
  async function ensureCleanOrClaimOnlyWhenNoAuto() {
@@ -116,6 +119,23 @@ async function ensureCleanOrClaimOnlyWhenNoAuto() {
116
119
  }
117
120
  }
118
121
  const PREFIX = LOG_PREFIX.CLAIM;
122
+ /**
123
+ * Resolve branch-aware cloud activation for wu:claim.
124
+ *
125
+ * This preserves source attribution from detectCloudMode while enforcing
126
+ * protected-branch behavior for explicit vs env-signal activation.
127
+ */
128
+ export function resolveCloudActivationForClaim(input) {
129
+ const detection = detectCloudMode({
130
+ cloudFlag: input.cloudFlag,
131
+ env: input.env,
132
+ config: input.config,
133
+ });
134
+ return resolveEffectiveCloudActivation({
135
+ detection,
136
+ currentBranch: input.currentBranch,
137
+ });
138
+ }
119
139
  /**
120
140
  * WU-1508: Enforce tests.manual at claim time for non-doc/process WUs.
121
141
  * This is non-bypassable (independent of --allow-incomplete) to fail early.
@@ -133,6 +153,27 @@ export function validateManualTestsForClaim(doc, id) {
133
153
  `Add at least one manual verification step under tests.manual before claiming.`,
134
154
  };
135
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
+ }
136
177
  /**
137
178
  * WU-1521: Build a rolled-back version of a WU YAML doc by stripping claim metadata.
138
179
  *
@@ -152,6 +193,7 @@ export function buildRollbackYamlDoc(doc) {
152
193
  rolled.status = WU_STATUS.READY;
153
194
  // Remove claim-specific metadata fields
154
195
  delete rolled.claimed_mode;
196
+ delete rolled.claimed_branch; // WU-1589: Clear claimed_branch on rollback
155
197
  delete rolled.claimed_at;
156
198
  delete rolled.worktree_path;
157
199
  delete rolled.baseline_main_sha;
@@ -159,6 +201,49 @@ export function buildRollbackYamlDoc(doc) {
159
201
  delete rolled.assigned_to;
160
202
  return rolled;
161
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
+ }
162
247
  /**
163
248
  * Pre-flight validation: Check WU file exists and is valid BEFORE any git operations
164
249
  * Prevents zombie worktrees when WU YAML is missing or malformed
@@ -181,7 +266,7 @@ function preflightValidateWU(WU_PATH, id) {
181
266
  }
182
267
  catch (e) {
183
268
  die(`Failed to parse WU YAML ${WU_PATH}\n\n` +
184
- `YAML parsing error: ${e.message}\n\n` +
269
+ `YAML parsing error: ${getErrorMessage(e)}\n\n` +
185
270
  `Fix the YAML syntax errors before claiming.`);
186
271
  }
187
272
  // Validate ID matches
@@ -192,7 +277,7 @@ function preflightValidateWU(WU_PATH, id) {
192
277
  `Fix the id field in the WU YAML before claiming.`);
193
278
  }
194
279
  // Validate state transition is allowed
195
- const currentStatus = doc.status || WU_STATUS.READY;
280
+ const currentStatus = resolveClaimStatus(doc.status);
196
281
  try {
197
282
  assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
198
283
  }
@@ -200,7 +285,7 @@ function preflightValidateWU(WU_PATH, id) {
200
285
  die(`Cannot claim ${id} - invalid state transition\n\n` +
201
286
  `Current status: ${currentStatus}\n` +
202
287
  `Attempted transition: ${currentStatus} → in_progress\n\n` +
203
- `Reason: ${error.message}`);
288
+ `Reason: ${getErrorMessage(error)}`);
204
289
  }
205
290
  return doc;
206
291
  }
@@ -252,7 +337,7 @@ function validateYAMLSchema(WU_PATH, doc, args) {
252
337
  }
253
338
  // WU-1576: validateBacklogConsistency removed - repair now happens inside micro-worktree
254
339
  // See claimWorktreeMode() execute function for the new location
255
- async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null, gitAdapter = null) {
340
+ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null, gitAdapter = null, claimedBranch = null) {
256
341
  // Check file exists
257
342
  try {
258
343
  await access(WU_PATH);
@@ -270,7 +355,7 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
270
355
  }
271
356
  catch (e) {
272
357
  die(`Failed to read WU file: ${WU_PATH}\n\n` +
273
- `Error: ${e.message}\n\n` +
358
+ `Error: ${getErrorMessage(e)}\n\n` +
274
359
  `Options:\n` +
275
360
  ` 1. Check file permissions: ls -la ${WU_PATH}\n` +
276
361
  ` 2. Ensure you have read access to the repository`);
@@ -281,7 +366,7 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
281
366
  }
282
367
  catch (e) {
283
368
  die(`Failed to parse YAML ${WU_PATH}\n\n` +
284
- `Error: ${e.message}\n\n` +
369
+ `Error: ${getErrorMessage(e)}\n\n` +
285
370
  `Options:\n` +
286
371
  ` 1. Validate YAML syntax: pnpm wu:validate --id ${id}\n` +
287
372
  ` 2. Fix YAML errors manually and retry`);
@@ -293,12 +378,12 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
293
378
  ` 2. Verify you're claiming the right WU`);
294
379
  }
295
380
  // Validate state transition before updating
296
- const currentStatus = doc.status || WU_STATUS.READY;
381
+ const currentStatus = resolveClaimStatus(doc.status);
297
382
  try {
298
383
  assertTransition(currentStatus, WU_STATUS.IN_PROGRESS, id);
299
384
  }
300
385
  catch (error) {
301
- die(`State transition validation failed: ${error.message}`);
386
+ die(`State transition validation failed: ${getErrorMessage(error)}`);
302
387
  }
303
388
  // Update status and lane (lane only if provided and different)
304
389
  doc.status = WU_STATUS.IN_PROGRESS;
@@ -306,6 +391,11 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
306
391
  doc.lane = lane;
307
392
  // Record claimed mode (worktree or branch-only)
308
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
+ }
309
399
  // WU-1226: Record worktree path to prevent resolution failures if lane field changes
310
400
  if (worktreePath) {
311
401
  doc.worktree_path = worktreePath;
@@ -386,7 +476,7 @@ async function maybeProgressInitiativeStatus(worktreePath, initiativeRef, wuId)
386
476
  }
387
477
  catch (error) {
388
478
  // Non-fatal: log warning and continue
389
- console.warn(`${PREFIX} ⚠️ Could not check initiative status progression: ${error.message}`);
479
+ console.warn(`${PREFIX} ⚠️ Could not check initiative status progression: ${getErrorMessage(error)}`);
390
480
  return { updated: false, initPath: null };
391
481
  }
392
482
  }
@@ -526,7 +616,8 @@ async function applyStagedChangesToMicroWorktree(worktreePath, stagedChanges) {
526
616
  * Ensures canonical state stays global while local main remains unchanged.
527
617
  */
528
618
  async function applyCanonicalClaimUpdate(ctx, sessionId) {
529
- const { args, id, laneK, worktree, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, fixableIssues, stagedChanges, } = ctx;
619
+ const { args, id, laneK, worktree, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, fixableIssues, stagedChanges, currentBranchForCloud, // WU-1590: For persisting claimed_branch
620
+ } = ctx;
530
621
  const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
531
622
  const worktreePathForYaml = claimedMode === CLAIMED_MODES.BRANCH_ONLY ? null : path.resolve(worktree);
532
623
  let updatedTitle = '';
@@ -554,7 +645,7 @@ async function applyCanonicalClaimUpdate(ctx, sessionId) {
554
645
  }
555
646
  const microGit = createGitForPath(worktreePath);
556
647
  // WU-1211: updateWUYaml now returns {title, initiative}
557
- const updateResult = await updateWUYaml(microWUPath, id, args.lane, claimedMode, worktreePathForYaml, sessionId, microGit);
648
+ const updateResult = await updateWUYaml(microWUPath, id, args.lane, claimedMode, worktreePathForYaml, sessionId, microGit, currentBranchForCloud || null);
558
649
  updatedTitle = updateResult.title || updatedTitle;
559
650
  await addOrReplaceInProgressStatus(microStatusPath, id, updatedTitle);
560
651
  await removeFromReadyAndAddToInProgressBacklog(microBacklogPath, id, updatedTitle, args.lane);
@@ -790,7 +881,7 @@ function validateLaneFormatWithError(lane) {
790
881
  validateLaneFormat(lane);
791
882
  }
792
883
  catch (error) {
793
- die(`Invalid lane format: ${error.message}\n\n` +
884
+ die(`Invalid lane format: ${getErrorMessage(error)}\n\n` +
794
885
  `Valid formats:\n` +
795
886
  ` - Parent-only: "Operations", "Intelligence", "Experience", etc.\n` +
796
887
  ` - Sub-lane: "Operations: Tooling", "Intelligence: Prompts", etc.\n\n` +
@@ -917,28 +1008,42 @@ async function validateBranchOnlyMode(STATUS_PATH, id) {
917
1008
  * Execute branch-only mode claim workflow
918
1009
  */
919
1010
  async function claimBranchOnlyMode(ctx) {
920
- const { args, id, laneK, title, branch, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, sessionId, updatedTitle, } = ctx;
921
- // Create branch and switch to it from origin/main (avoids local main mutation)
922
- try {
923
- await getGitForCwd().createBranch(branch, `${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
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
+ }
924
1026
  }
925
- catch (error) {
926
- die(`Canonical claim state may be updated, but branch creation failed.\n\n` +
927
- `Error: ${error.message}\n\n` +
928
- `Recovery:\n` +
929
- ` 1. Run: git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}\n` +
930
- ` 2. Retry: pnpm wu:claim --id ${id} --lane "${args.lane}"\n` +
931
- ` 3. If needed, delete local branch: git branch -D ${branch}`);
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.`);
932
1032
  }
933
1033
  let finalTitle = updatedTitle || title;
934
1034
  const msg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
935
- if (args.noPush) {
1035
+ const shouldPersistClaimMetadata = shouldPersistClaimMetadataOnBranch({
1036
+ claimedMode,
1037
+ noPush: Boolean(args.noPush),
1038
+ });
1039
+ if (shouldPersistClaimMetadata) {
936
1040
  if (args.noAuto) {
937
1041
  await ensureCleanOrClaimOnlyWhenNoAuto();
938
1042
  }
939
1043
  else {
940
1044
  // WU-1211: updateWUYaml now returns {title, initiative}
941
- const updateResult = await updateWUYaml(WU_PATH, id, args.lane, claimedMode, null, sessionId);
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);
942
1047
  finalTitle = updateResult.title || finalTitle;
943
1048
  await addOrReplaceInProgressStatus(STATUS_PATH, id, finalTitle);
944
1049
  await removeFromReadyAndAddToInProgressBacklog(BACKLOG_PATH, id, finalTitle, args.lane);
@@ -950,9 +1055,11 @@ async function claimBranchOnlyMode(ctx) {
950
1055
  filesToAdd.push(initProgress.initPath);
951
1056
  }
952
1057
  }
953
- await getGitForCwd().add(filesToAdd.map((f) => JSON.stringify(f)).join(' '));
1058
+ await getGitForCwd().add(filesToAdd);
954
1059
  }
955
1060
  await getGitForCwd().commit(msg);
1061
+ }
1062
+ if (args.noPush) {
956
1063
  console.warn(`${PREFIX} Warning: --no-push enabled. Claim is local-only and NOT visible to other agents.`);
957
1064
  }
958
1065
  else {
@@ -1323,6 +1430,33 @@ async function main() {
1323
1430
  if (!PATTERNS.WU_ID.test(id))
1324
1431
  die(`Invalid WU id '${args.id}'. Expected format WU-123`);
1325
1432
  await ensureOnMain(getGitForCwd());
1433
+ // WU-1609: Resolve branch-aware cloud activation at preflight so explicit
1434
+ // protected-branch cloud requests fail before lane locking/state mutation.
1435
+ const preflightBranch = await getGitForCwd().getCurrentBranch();
1436
+ const preflightCloudEffective = resolveCloudActivationForClaim({
1437
+ cloudFlag: Boolean(args.cloud),
1438
+ env: process.env,
1439
+ config: getConfig().cloud,
1440
+ currentBranch: preflightBranch,
1441
+ });
1442
+ if (preflightCloudEffective.blocked) {
1443
+ const sourceHint = preflightCloudEffective.source === CLOUD_ACTIVATION_SOURCE.FLAG
1444
+ ? '--cloud'
1445
+ : 'LUMENFLOW_CLOUD=1';
1446
+ die(`Cloud mode blocked on protected branch "${preflightBranch}".\n\n` +
1447
+ `Explicit cloud activation (${sourceHint}) is not allowed on main/master.\n` +
1448
+ `Switch to a non-main branch for cloud mode, or run wu:claim without cloud activation on main/master.`);
1449
+ }
1450
+ if (preflightCloudEffective.suppressed) {
1451
+ const signalSuffix = preflightCloudEffective.matchedSignal
1452
+ ? ` (signal: ${preflightCloudEffective.matchedSignal})`
1453
+ : '';
1454
+ console.log(`${PREFIX} Cloud auto-detection suppressed on protected branch "${preflightBranch}"${signalSuffix}; continuing with standard claim flow.`);
1455
+ }
1456
+ else if (preflightCloudEffective.isCloud &&
1457
+ preflightCloudEffective.source === CLOUD_ACTIVATION_SOURCE.ENV_SIGNAL) {
1458
+ console.log(`${PREFIX} Cloud mode auto-detected (source: ${preflightCloudEffective.source}${preflightCloudEffective.matchedSignal ? `, signal: ${preflightCloudEffective.matchedSignal}` : ''})`);
1459
+ }
1326
1460
  // WU-2411: Handle --resume flag for agent handoff
1327
1461
  if (args.resume) {
1328
1462
  await handleResumeMode(args, id);
@@ -1449,18 +1583,11 @@ async function main() {
1449
1583
  const title = (await readWUTitle(id)) || '';
1450
1584
  const branch = args.branch || `lane/${laneK}/${idK}`;
1451
1585
  const worktree = args.worktree || `worktrees/${laneK}-${idK}`;
1452
- // WU-1495: Cloud auto-detection from config-driven env signals
1453
- // Detection precedence: --cloud flag > LUMENFLOW_CLOUD=1 > env_signals (opt-in)
1454
- const config = getConfig();
1455
- const cloudDetection = detectCloudMode({
1456
- cloudFlag: Boolean(args.cloud),
1457
- env: process.env,
1458
- config: config.cloud,
1459
- });
1460
- const effectiveCloud = cloudDetection.isCloud;
1461
- if (cloudDetection.isCloud && !args.cloud) {
1462
- console.log(`${PREFIX} Cloud mode auto-detected (source: ${cloudDetection.source}${cloudDetection.matchedSignal ? `, signal: ${cloudDetection.matchedSignal}` : ''})`);
1463
- }
1586
+ const currentBranch = preflightBranch;
1587
+ const cloudEffective = preflightCloudEffective;
1588
+ const effectiveCloud = cloudEffective.isCloud;
1589
+ // WU-1590: Capture current branch for cloud claim metadata (before any branch switching)
1590
+ const currentBranchForCloud = effectiveCloud ? currentBranch : undefined;
1464
1591
  // WU-1491: Resolve claimed mode from flag combination
1465
1592
  const modeResult = resolveClaimMode({
1466
1593
  branchOnly: args.branchOnly,
@@ -1476,11 +1603,24 @@ async function main() {
1476
1603
  if (!modeResult.skipBranchOnlySingletonGuard) {
1477
1604
  await validateBranchOnlyMode(STATUS_PATH, id);
1478
1605
  }
1606
+ // WU-1590: Skip branch-exists checks in cloud mode (branch already exists by definition)
1607
+ const branchExecution = resolveBranchClaimExecution({
1608
+ claimedMode,
1609
+ isCloud: effectiveCloud,
1610
+ currentBranch,
1611
+ requestedBranch: branch,
1612
+ });
1613
+ const effectiveBranch = branchExecution.executionBranch;
1614
+ const skipBranchChecks = shouldSkipBranchExistsCheck({
1615
+ isCloud: effectiveCloud,
1616
+ currentBranch,
1617
+ laneBranch: effectiveBranch,
1618
+ });
1479
1619
  // Check if remote branch already exists (prevents duplicate global claims)
1480
- if (!args.noPush) {
1481
- const remoteExists = await getGitForCwd().remoteBranchExists(REMOTES.ORIGIN, branch);
1620
+ if (!args.noPush && !skipBranchChecks) {
1621
+ const remoteExists = await getGitForCwd().remoteBranchExists(REMOTES.ORIGIN, effectiveBranch);
1482
1622
  if (remoteExists) {
1483
- die(`Remote branch ${REMOTES.ORIGIN}/${branch} already exists. WU may already be claimed.\n\n` +
1623
+ die(`Remote branch ${REMOTES.ORIGIN}/${effectiveBranch} already exists. WU may already be claimed.\n\n` +
1484
1624
  `Options:\n` +
1485
1625
  ` 1. Coordinate with the owning agent or wait for completion\n` +
1486
1626
  ` 2. Choose a different WU\n` +
@@ -1488,14 +1628,16 @@ async function main() {
1488
1628
  }
1489
1629
  }
1490
1630
  // Check if branch already exists locally (prevents duplicate claims)
1491
- const branchAlreadyExists = await getGitForCwd().branchExists(branch);
1492
- if (branchAlreadyExists) {
1493
- die(`Branch ${branch} already exists. WU may already be claimed.\n\n` +
1494
- `Git branch existence = WU claimed (natural locking).\n\n` +
1495
- `Options:\n` +
1496
- ` 1. Check git worktree list to see if worktree exists\n` +
1497
- ` 2. Coordinate with the owning agent or wait for them to complete\n` +
1498
- ` 3. Choose a different WU`);
1631
+ if (!skipBranchChecks) {
1632
+ const branchAlreadyExists = await getGitForCwd().branchExists(effectiveBranch);
1633
+ if (branchAlreadyExists) {
1634
+ die(`Branch ${effectiveBranch} already exists. WU may already be claimed.\n\n` +
1635
+ `Git branch existence = WU claimed (natural locking).\n\n` +
1636
+ `Options:\n` +
1637
+ ` 1. Check git worktree list to see if worktree exists\n` +
1638
+ ` 2. Coordinate with the owning agent or wait for them to complete\n` +
1639
+ ` 3. Choose a different WU`);
1640
+ }
1499
1641
  }
1500
1642
  // Layer 3 defense (WU-1476): Pre-flight orphan check
1501
1643
  // Clean up orphan directory if it exists at target worktree path
@@ -1508,7 +1650,7 @@ async function main() {
1508
1650
  }
1509
1651
  catch (err) {
1510
1652
  die(`Failed to clean up orphan directory at ${worktree}\n\n` +
1511
- `Error: ${err.message}\n\n` +
1653
+ `Error: ${getErrorMessage(err)}\n\n` +
1512
1654
  `Manual cleanup: rm -rf ${absoluteWorktreePath}`);
1513
1655
  }
1514
1656
  }
@@ -1529,7 +1671,7 @@ async function main() {
1529
1671
  }
1530
1672
  catch (err) {
1531
1673
  // Non-blocking: session start failure should not block claim
1532
- console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
1674
+ console.warn(`${PREFIX} Warning: Could not start agent session: ${getErrorMessage(err)}`);
1533
1675
  }
1534
1676
  // Execute claim workflow
1535
1677
  const baseCtx = {
@@ -1537,18 +1679,26 @@ async function main() {
1537
1679
  id,
1538
1680
  laneK,
1539
1681
  title,
1540
- branch,
1682
+ branch: effectiveBranch,
1541
1683
  worktree,
1542
1684
  WU_PATH,
1543
1685
  STATUS_PATH,
1544
1686
  BACKLOG_PATH,
1545
1687
  claimedMode,
1688
+ shouldCreateBranch: branchExecution.shouldCreateBranch,
1689
+ currentBranch,
1546
1690
  fixableIssues, // WU-1361: Pass fixable issues for worktree application
1547
1691
  stagedChanges,
1692
+ currentBranchForCloud, // WU-1590: For persisting claimed_branch in branch-pr mode
1548
1693
  };
1549
1694
  let updatedTitle = title;
1550
1695
  claimTitle = title;
1551
- if (!args.noPush) {
1696
+ const shouldApplyCanonicalUpdate = shouldApplyCanonicalClaimUpdate({
1697
+ isCloud: effectiveCloud,
1698
+ claimedMode,
1699
+ noPush: Boolean(args.noPush),
1700
+ });
1701
+ if (shouldApplyCanonicalUpdate) {
1552
1702
  updatedTitle = (await applyCanonicalClaimUpdate(baseCtx, sessionId)) || updatedTitle;
1553
1703
  // WU-1521: Mark that canonical claim was pushed to origin/main
1554
1704
  // If claim fails after this point, the finally block will rollback
@@ -1557,6 +1707,9 @@ async function main() {
1557
1707
  // Refresh origin/main after push-only update so worktrees start from canonical state
1558
1708
  await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
1559
1709
  }
1710
+ else if (!args.noPush && claimedMode === CLAIMED_MODES.BRANCH_PR) {
1711
+ console.log(`${PREFIX} Skipping canonical claim update on origin/main for cloud branch-pr claim.`);
1712
+ }
1560
1713
  const ctx = {
1561
1714
  ...baseCtx,
1562
1715
  sessionId,
@@ -1570,6 +1723,30 @@ async function main() {
1570
1723
  else {
1571
1724
  await claimWorktreeMode(ctx);
1572
1725
  }
1726
+ // WU-1605: Record claim-time pickup evidence for delegation provenance.
1727
+ // Non-blocking: this metadata should not block claim completion.
1728
+ try {
1729
+ let claimedBy = process.env.GIT_AUTHOR_EMAIL?.trim();
1730
+ try {
1731
+ claimedBy = await getAssignedEmail(getGitForCwd());
1732
+ }
1733
+ catch {
1734
+ // Fall back to env/default when git email lookup fails in this context.
1735
+ }
1736
+ const pickupResult = await recordClaimPickupEvidence(id, {
1737
+ baseDir: process.cwd(),
1738
+ claimedBy,
1739
+ });
1740
+ if (pickupResult.recorded) {
1741
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Recorded delegation pickup evidence (${pickupResult.spawnId})`);
1742
+ }
1743
+ else if (pickupResult.alreadyRecorded) {
1744
+ console.log(`${PREFIX} ${EMOJI.INFO} Delegation pickup evidence already recorded (${pickupResult.spawnId})`);
1745
+ }
1746
+ }
1747
+ catch (err) {
1748
+ console.warn(`${PREFIX} Warning: Could not record delegation pickup evidence: ${getErrorMessage(err)}`);
1749
+ }
1573
1750
  // Mark claim as successful - lock should remain for wu:done to release
1574
1751
  claimSucceeded = true;
1575
1752
  }