@lumenflow/cli 1.0.0 → 1.3.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/dist/wu-claim.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * Full migration to thin shim pending @lumenflow/core CLI export implementation.
16
16
  */
17
17
  import { existsSync, readFileSync, rmSync } from 'node:fs';
18
- import { access, readFile, writeFile } from 'node:fs/promises';
18
+ import { access, readFile, writeFile, mkdir } from 'node:fs/promises';
19
19
  import path from 'node:path';
20
20
  import { isOrphanWorktree } from '@lumenflow/core/dist/orphan-detector.js';
21
21
  // WU-1352: Use centralized YAML functions from wu-yaml.mjs
@@ -32,14 +32,15 @@ import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter
32
32
  import { die } from '@lumenflow/core/dist/error-handler.js';
33
33
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
34
34
  import { WU_PATHS, getStateStoreDirFromBacklog } from '@lumenflow/core/dist/wu-paths.js';
35
- import { BRANCHES, REMOTES, WU_STATUS, CLAIMED_MODES, STATUS_SECTIONS, PATTERNS, toKebab, LOG_PREFIX, MICRO_WORKTREE_OPERATIONS, COMMIT_FORMATS, EMOJI, FILE_SYSTEM, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
35
+ 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, } from '@lumenflow/core/dist/wu-constants.js';
36
36
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
37
- import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
37
+ import { ensureOnMain, ensureMainUpToDate } from '@lumenflow/core/dist/wu-helpers.js';
38
38
  import { emitWUFlowEvent } from '@lumenflow/core/dist/telemetry.js';
39
39
  import { checkLaneForOrphanDoneWU, repairWUInconsistency, } from '@lumenflow/core/dist/wu-consistency-checker.js';
40
40
  import { emitMandatoryAgentAdvisory } from '@lumenflow/core/dist/orchestration-advisory-loader.js';
41
41
  import { validateWU, generateAutoApproval } from '@lumenflow/core/dist/wu-schema.js';
42
42
  import { startSessionForWU } from '@lumenflow/agent/dist/auto-session-integration.js';
43
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
43
44
  import { detectFixableIssues, applyFixes, autoFixWUYaml, formatIssues, } from '@lumenflow/core/dist/wu-yaml-fixer.js';
44
45
  import { validateSpecCompleteness } from '@lumenflow/core/dist/wu-done-validators.js';
45
46
  import { getAssignedEmail } from '@lumenflow/core/dist/wu-claim-helpers.js';
@@ -164,7 +165,7 @@ function validateYAMLSchema(WU_PATH, doc, args) {
164
165
  }
165
166
  // WU-1576: validateBacklogConsistency removed - repair now happens inside micro-worktree
166
167
  // See claimWorktreeMode() execute function for the new location
167
- async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null) {
168
+ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktreePath = null, sessionId = null, gitAdapter = null) {
168
169
  // Check file exists
169
170
  try {
170
171
  await access(WU_PATH);
@@ -223,16 +224,16 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
223
224
  if (worktreePath) {
224
225
  doc.worktree_path = worktreePath;
225
226
  }
227
+ const git = gitAdapter || getGitForCwd();
226
228
  // WU-1423: Record owner using validated email (no silent username fallback)
227
229
  // Fallback chain: git config user.email > GIT_AUTHOR_EMAIL > error
228
230
  // WU-1427: getAssignedEmail is now async to properly await gitAdapter.getConfigValue
229
- doc.assigned_to = await getAssignedEmail(getGitForCwd());
231
+ doc.assigned_to = await getAssignedEmail(git);
230
232
  // Record claim timestamp for duration tracking (WU-637)
231
233
  doc.claimed_at = new Date().toISOString();
232
234
  // WU-1382: Store baseline main SHA for parallel agent detection
233
235
  // wu:done will compare against this to detect if other WUs were merged during work
234
- const git = getGitForCwd();
235
- doc.baseline_main_sha = await git.getCommitHash('origin/main');
236
+ doc.baseline_main_sha = await git.getCommitHash(GIT_REFS.ORIGIN_MAIN);
236
237
  // WU-1438: Store agent session ID for tracking
237
238
  if (sessionId) {
238
239
  doc.session_id = sessionId;
@@ -357,6 +358,90 @@ export function getWorktreeCommitFiles(wuId) {
357
358
  // These generated files cause merge conflicts when main advances
358
359
  ];
359
360
  }
361
+ function parseStagedChangeLine(line) {
362
+ const parts = line.trim().split(/\s+/);
363
+ const status = parts[0];
364
+ if (!status)
365
+ return null;
366
+ if (status.startsWith('R') || status.startsWith('C')) {
367
+ return { status, from: parts[1], filePath: parts[2] };
368
+ }
369
+ return { status, filePath: parts.slice(1).join(' ') };
370
+ }
371
+ async function getStagedChanges() {
372
+ const diff = await getGitForCwd().raw(['diff', '--cached', '--name-status']);
373
+ if (!diff.trim())
374
+ return [];
375
+ return diff
376
+ .split(STRING_LITERALS.NEWLINE)
377
+ .filter(Boolean)
378
+ .map(parseStagedChangeLine)
379
+ .filter(Boolean);
380
+ }
381
+ async function applyStagedChangesToMicroWorktree(worktreePath, stagedChanges) {
382
+ for (const change of stagedChanges) {
383
+ const filePath = change.filePath;
384
+ if (!filePath)
385
+ continue;
386
+ const targetPath = path.join(worktreePath, filePath);
387
+ if (change.status.startsWith('D')) {
388
+ rmSync(targetPath, { recursive: true, force: true });
389
+ continue;
390
+ }
391
+ const sourcePath = path.join(process.cwd(), filePath);
392
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
393
+ const contents = await readFile(sourcePath, { encoding: FILE_SYSTEM.UTF8 });
394
+ await mkdir(path.dirname(targetPath), { recursive: true });
395
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
396
+ await writeFile(targetPath, contents, { encoding: FILE_SYSTEM.UTF8 });
397
+ }
398
+ }
399
+ /**
400
+ * Update canonical claim state on origin/main using push-only micro-worktree.
401
+ * Ensures canonical state stays global while local main remains unchanged.
402
+ */
403
+ async function applyCanonicalClaimUpdate(ctx, sessionId) {
404
+ const { args, id, laneK, worktree, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, fixableIssues, stagedChanges, } = ctx;
405
+ const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
406
+ const worktreePathForYaml = claimedMode === CLAIMED_MODES.BRANCH_ONLY ? null : path.resolve(worktree);
407
+ let updatedTitle = '';
408
+ const filesToCommit = args.noAuto && stagedChanges.length > 0
409
+ ? stagedChanges.map((change) => change.filePath).filter(Boolean)
410
+ : [WU_PATHS.WU(id), WU_PATHS.STATUS(), WU_PATHS.BACKLOG(), '.beacon/state/wu-events.jsonl'];
411
+ console.log(`${PREFIX} Updating canonical claim state (push-only)...`);
412
+ await withMicroWorktree({
413
+ operation: MICRO_WORKTREE_OPERATIONS.WU_CLAIM,
414
+ id,
415
+ logPrefix: PREFIX,
416
+ pushOnly: true,
417
+ execute: async ({ worktreePath }) => {
418
+ const microWUPath = path.join(worktreePath, WU_PATH);
419
+ const microStatusPath = path.join(worktreePath, STATUS_PATH);
420
+ const microBacklogPath = path.join(worktreePath, BACKLOG_PATH);
421
+ if (args.noAuto) {
422
+ await applyStagedChangesToMicroWorktree(worktreePath, stagedChanges);
423
+ }
424
+ else {
425
+ if (fixableIssues && fixableIssues.length > 0) {
426
+ console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
427
+ autoFixWUYaml(microWUPath);
428
+ console.log(`${PREFIX} YAML fixes applied successfully`);
429
+ }
430
+ const microGit = createGitForPath(worktreePath);
431
+ updatedTitle =
432
+ (await updateWUYaml(microWUPath, id, args.lane, claimedMode, worktreePathForYaml, sessionId, microGit)) || updatedTitle;
433
+ await addOrReplaceInProgressStatus(microStatusPath, id, updatedTitle);
434
+ await removeFromReadyAndAddToInProgressBacklog(microBacklogPath, id, updatedTitle, args.lane);
435
+ }
436
+ return {
437
+ commitMessage: commitMsg,
438
+ files: filesToCommit,
439
+ };
440
+ },
441
+ });
442
+ console.log(`${PREFIX} Canonical claim state updated on origin/main`);
443
+ return updatedTitle;
444
+ }
360
445
  async function readWUTitle(id) {
361
446
  const p = WU_PATHS.WU(id);
362
447
  // Check file exists
@@ -507,7 +592,10 @@ function validateLaneFormatWithError(lane) {
507
592
  }
508
593
  }
509
594
  /**
510
- * Handle lane occupancy check and enforce WIP=1 policy
595
+ * Handle lane occupancy check and enforce WIP limit policy
596
+ *
597
+ * WU-1016: Updated to support configurable WIP limits per lane.
598
+ * The WIP limit is read from .lumenflow.config.yaml and defaults to 1.
511
599
  */
512
600
  function handleLaneOccupancy(laneCheck, lane, id, force) {
513
601
  if (laneCheck.free)
@@ -517,18 +605,26 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
517
605
  }
518
606
  if (!laneCheck.occupiedBy)
519
607
  return;
608
+ // WU-1016: Include WIP limit info in messages
609
+ const wipLimit = laneCheck.wipLimit ?? 1;
610
+ const currentCount = laneCheck.currentCount ?? 0;
611
+ const inProgressList = laneCheck.inProgressWUs?.join(', ') || laneCheck.occupiedBy;
520
612
  if (force) {
521
- console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}" is occupied by ${laneCheck.occupiedBy}`);
522
- console.warn(`${PREFIX} ⚠️ Forcing WIP=2 in same lane. Risk of worktree collision!`);
613
+ console.warn(`${PREFIX} ⚠️ WARNING: Lane "${lane}" has ${currentCount}/${wipLimit} WUs in progress`);
614
+ console.warn(`${PREFIX} ⚠️ In progress: ${inProgressList}`);
615
+ console.warn(`${PREFIX} ⚠️ Forcing WIP limit override. Risk of worktree collision!`);
523
616
  console.warn(`${PREFIX} ⚠️ Use only for P0 emergencies or manual recovery.`);
524
617
  return;
525
618
  }
526
- die(`Lane "${lane}" is already occupied by ${laneCheck.occupiedBy}.\n\n` +
527
- `LumenFlow enforces one-WU-per-lane to maintain focus.\n\n` +
619
+ die(`Lane "${lane}" is at WIP limit (${currentCount}/${wipLimit}).\n\n` +
620
+ `In progress: ${inProgressList}\n\n` +
621
+ `LumenFlow enforces WIP limits per lane to maintain focus.\n` +
622
+ `Current limit for "${lane}": ${wipLimit} (configure in .lumenflow.config.yaml)\n\n` +
528
623
  `Options:\n` +
529
- ` 1. Wait for ${laneCheck.occupiedBy} to complete or block\n` +
624
+ ` 1. Wait for a WU to complete or block\n` +
530
625
  ` 2. Choose a different lane\n` +
531
- ` 3. Use --force to override (P0 emergencies only)\n\n` +
626
+ ` 3. Increase wip_limit in .lumenflow.config.yaml\n` +
627
+ ` 4. Use --force to override (P0 emergencies only)\n\n` +
532
628
  `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
533
629
  }
534
630
  /**
@@ -606,51 +702,44 @@ async function validateBranchOnlyMode(STATUS_PATH, id) {
606
702
  * Execute branch-only mode claim workflow
607
703
  */
608
704
  async function claimBranchOnlyMode(ctx) {
609
- const { args, id, laneK, title, branch, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode } = ctx;
610
- // WU-1438: Start agent session BEFORE metadata update to include session_id in YAML
611
- let sessionId = null;
705
+ const { args, id, laneK, title, branch, WU_PATH, STATUS_PATH, BACKLOG_PATH, claimedMode, sessionId, updatedTitle, } = ctx;
706
+ // Create branch and switch to it from origin/main (avoids local main mutation)
612
707
  try {
613
- const sessionResult = await startSessionForWU({
614
- wuId: id,
615
- tier: 2,
616
- });
617
- sessionId = sessionResult.sessionId;
618
- if (sessionResult.alreadyActive) {
619
- console.log(`${PREFIX} Agent session already active (${sessionId.slice(0, 8)}...)`);
708
+ await getGitForCwd().createBranch(branch, `${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
709
+ }
710
+ catch (error) {
711
+ die(`Canonical claim state may be updated, but branch creation failed.\n\n` +
712
+ `Error: ${error.message}\n\n` +
713
+ `Recovery:\n` +
714
+ ` 1. Run: git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}\n` +
715
+ ` 2. Retry: pnpm wu:claim --id ${id} --lane "${args.lane}"\n` +
716
+ ` 3. If needed, delete local branch: git branch -D ${branch}`);
717
+ }
718
+ let finalTitle = updatedTitle || title;
719
+ const msg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
720
+ if (args.noPush) {
721
+ if (args.noAuto) {
722
+ await ensureCleanOrClaimOnlyWhenNoAuto();
620
723
  }
621
724
  else {
622
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Agent session started (${sessionId.slice(0, 8)}...)`);
725
+ finalTitle =
726
+ (await updateWUYaml(WU_PATH, id, args.lane, claimedMode, null, sessionId)) || finalTitle;
727
+ await addOrReplaceInProgressStatus(STATUS_PATH, id, finalTitle);
728
+ await removeFromReadyAndAddToInProgressBacklog(BACKLOG_PATH, id, finalTitle, args.lane);
729
+ await getGitForCwd().add(`${JSON.stringify(WU_PATH)} ${JSON.stringify(STATUS_PATH)} ${JSON.stringify(BACKLOG_PATH)}`);
623
730
  }
624
- }
625
- catch (err) {
626
- // Non-blocking: session start failure should not block claim
627
- console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
628
- }
629
- // Create branch and switch to it (LEGACY - for constrained environments only)
630
- await getGitForCwd().createBranch(branch, BRANCHES.MAIN);
631
- // Update metadata in branch-only mode (on main checkout)
632
- let updatedTitle = title;
633
- if (args.noAuto) {
634
- await ensureCleanOrClaimOnlyWhenNoAuto();
731
+ await getGitForCwd().commit(msg);
732
+ console.warn(`${PREFIX} Warning: --no-push enabled. Claim is local-only and NOT visible to other agents.`);
635
733
  }
636
734
  else {
637
- updatedTitle =
638
- (await updateWUYaml(WU_PATH, id, args.lane, claimedMode, null, sessionId)) || title;
639
- await addOrReplaceInProgressStatus(STATUS_PATH, id, updatedTitle);
640
- await removeFromReadyAndAddToInProgressBacklog(BACKLOG_PATH, id, updatedTitle, args.lane);
641
- await getGitForCwd().add(`${JSON.stringify(WU_PATH)} ${JSON.stringify(STATUS_PATH)} ${JSON.stringify(BACKLOG_PATH)}`);
735
+ await getGitForCwd().push(REMOTES.ORIGIN, branch, { setUpstream: true });
642
736
  }
643
- // Commit and push
644
- const msg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
645
- await getGitForCwd().commit(msg);
646
- await getGitForCwd().push(REMOTES.ORIGIN, branch);
647
737
  // Summary
648
738
  console.log(`\n${PREFIX} Claim recorded in Branch-Only mode.`);
649
- console.log(`- WU: ${id}${updatedTitle ? ` — ${updatedTitle}` : ''}`);
739
+ console.log(`- WU: ${id}${finalTitle ? ` — ${finalTitle}` : ''}`);
650
740
  console.log(`- Lane: ${args.lane}`);
651
741
  console.log(`- Mode: Branch-Only (no worktree)`);
652
- console.log(`- Commit: ${msg}`);
653
- console.log(`- Branch: ${branch}`);
742
+ console.log(`${args.noPush ? `- Commit: ${msg}` : `- Branch: ${branch}`}`);
654
743
  console.log('\n⚠️ LIMITATION: Branch-Only mode does not support parallel WUs (WIP=1 across ALL lanes)');
655
744
  console.log('Next: work on this branch in the main checkout.');
656
745
  // WU-1360: Print next-steps checklist to prevent common mistakes
@@ -695,97 +784,95 @@ async function claimBranchOnlyMode(ctx) {
695
784
  */
696
785
  async function claimWorktreeMode(ctx) {
697
786
  const { args, id, laneK, title, branch, worktree, WU_PATH, BACKLOG_PATH, claimedMode, fixableIssues, // Fixable issues from pre-flight validation
698
- } = ctx;
787
+ sessionId, updatedTitle, stagedChanges, } = ctx;
699
788
  const originalCwd = process.cwd();
700
789
  const worktreePath = path.resolve(worktree);
701
- let updatedTitle = title;
790
+ let finalTitle = updatedTitle || title;
702
791
  const commitMsg = COMMIT_FORMATS.CLAIM(id.toLowerCase(), laneK);
703
- // WU-1438: Start agent session BEFORE metadata update to include session_id in YAML
704
- let sessionId = null;
705
- try {
706
- const sessionResult = await startSessionForWU({
707
- wuId: id,
708
- tier: 2,
709
- });
710
- sessionId = sessionResult.sessionId;
711
- if (sessionResult.alreadyActive) {
712
- console.log(`${PREFIX} Agent session already active (${sessionId.slice(0, 8)}...)`);
713
- }
714
- else {
715
- console.log(`${PREFIX} ${EMOJI.SUCCESS} Agent session started (${sessionId.slice(0, 8)}...)`);
716
- }
717
- }
718
- catch (err) {
719
- // Non-blocking: session start failure should not block claim
720
- console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
721
- }
722
792
  // WU-1741: Step 1 - Create work worktree+branch from main
723
793
  // Branch creation IS the coordination lock (git prevents duplicate branch names)
724
794
  console.log(`${PREFIX} Creating worktree (branch = coordination lock)...`);
725
- await getGitForCwd().worktreeAdd(worktree, branch, BRANCHES.MAIN);
795
+ const startPoint = args.noPush ? BRANCHES.MAIN : `${REMOTES.ORIGIN}/${BRANCHES.MAIN}`;
796
+ await getGitForCwd().worktreeAdd(worktree, branch, startPoint);
726
797
  console.log(`${PREFIX} ${EMOJI.SUCCESS} Worktree created at ${worktree}`);
727
- // WU-1741: Step 2 - Update metadata IN the work worktree (not main)
728
- if (!args.noAuto) {
729
- // Build paths relative to work worktree
730
- const wtWUPath = path.join(worktreePath, WU_PATH);
731
- const wtBacklogPath = path.join(worktreePath, BACKLOG_PATH);
732
- // Apply YAML fixes in worktree (not on main)
733
- if (fixableIssues && fixableIssues.length > 0) {
734
- console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
735
- autoFixWUYaml(wtWUPath);
736
- console.log(`${PREFIX} YAML fixes applied successfully`);
798
+ if (!args.noPush) {
799
+ const wtGit = createGitForPath(worktreePath);
800
+ await wtGit.push(REMOTES.ORIGIN, branch, { setUpstream: true });
801
+ }
802
+ if (args.noPush) {
803
+ // Local-only claim (air-gapped) - update metadata inside worktree branch
804
+ if (args.noAuto) {
805
+ await applyStagedChangesToMicroWorktree(worktreePath, stagedChanges);
806
+ }
807
+ else {
808
+ const wtWUPath = path.join(worktreePath, WU_PATH);
809
+ const wtBacklogPath = path.join(worktreePath, BACKLOG_PATH);
810
+ if (fixableIssues && fixableIssues.length > 0) {
811
+ console.log(`${PREFIX} Applying ${fixableIssues.length} YAML fix(es)...`);
812
+ autoFixWUYaml(wtWUPath);
813
+ console.log(`${PREFIX} YAML fixes applied successfully`);
814
+ }
815
+ finalTitle =
816
+ (await updateWUYaml(wtWUPath, id, args.lane, claimedMode, worktree, sessionId)) ||
817
+ finalTitle;
818
+ // WU-1746: Only append claim event to state store - don't regenerate backlog.md/status.md
819
+ const wtStateDir = getStateStoreDirFromBacklog(wtBacklogPath);
820
+ await appendClaimEventOnly(wtStateDir, id, finalTitle, args.lane);
737
821
  }
738
- // Update metadata files in worktree (WU-1438: include session_id)
739
- updatedTitle =
740
- (await updateWUYaml(wtWUPath, id, args.lane, claimedMode, worktree, sessionId)) || title;
741
- // WU-1746: Only append claim event to state store - don't regenerate backlog.md/status.md
742
- // These generated files cause merge conflicts when committed to worktrees
743
- const wtStateDir = getStateStoreDirFromBacklog(wtBacklogPath);
744
- await appendClaimEventOnly(wtStateDir, id, updatedTitle, args.lane);
745
- // WU-1741: Step 3 - Commit metadata in worktree (NOT on main)
746
- // This commit stays on the lane branch until wu:done merges to main
747
822
  console.log(`${PREFIX} Committing claim metadata in worktree...`);
748
823
  const wtGit = createGitForPath(worktreePath);
749
- // WU-1746: Use getWorktreeCommitFiles which excludes backlog.md and status.md
750
824
  const filesToCommit = getWorktreeCommitFiles(id);
751
825
  await wtGit.add(filesToCommit);
752
826
  await wtGit.commit(commitMsg);
753
827
  console.log(`${PREFIX} ${EMOJI.SUCCESS} Claim committed: ${commitMsg}`);
828
+ console.warn(`${PREFIX} Warning: --no-push enabled. Claim is local-only and NOT visible to other agents.`);
754
829
  }
755
- // WU-1443: Auto-symlink node_modules for immediate pnpm usability
756
- // WU-2238: Pass mainRepoPath to detect broken worktree-path symlinks
757
- const symlinkResult = symlinkNodeModules(worktreePath, console, originalCwd);
758
- if (symlinkResult.created) {
759
- console.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked for immediate use`);
830
+ // WU-1023: Auto-setup worktree dependencies
831
+ // By default, run pnpm install to ensure all dependencies are built (including CLI dist)
832
+ // Use --skip-setup to use symlink-only approach for faster claims when deps are already built
833
+ if (args.skipSetup) {
834
+ // WU-1443: Symlink-only mode for fast claims
835
+ // WU-2238: Pass mainRepoPath to detect broken worktree-path symlinks
836
+ const symlinkResult = symlinkNodeModules(worktreePath, console, originalCwd);
837
+ if (symlinkResult.created) {
838
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked (--skip-setup mode)`);
839
+ }
840
+ else if (symlinkResult.refused) {
841
+ console.warn(`${PREFIX} Warning: symlink refused: ${symlinkResult.reason}`);
842
+ console.warn(`${PREFIX} Run 'pnpm install' manually in the worktree`);
843
+ }
844
+ // WU-1579: Auto-symlink nested package node_modules for turbo typecheck
845
+ if (!symlinkResult.refused) {
846
+ const nestedResult = symlinkNestedNodeModules(worktreePath, originalCwd);
847
+ if (nestedResult.created > 0) {
848
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
849
+ }
850
+ }
760
851
  }
761
- else if (symlinkResult.refused) {
762
- // WU-2238: Symlinking was refused due to worktree-path symlinks
763
- // Fall back to running pnpm install in the worktree
764
- console.log(`${PREFIX} Running pnpm install in worktree (symlink refused: ${symlinkResult.reason})`);
852
+ else {
853
+ // WU-1023: Full setup mode (default) - run pnpm install with progress indicator
854
+ // This ensures CLI dist is built and all dependencies are properly resolved
855
+ console.log(`${PREFIX} Installing worktree dependencies (this may take a moment)...`);
765
856
  try {
766
857
  const { execSync } = await import('node:child_process');
767
858
  execSync('pnpm install --frozen-lockfile', {
768
859
  cwd: worktreePath,
769
- stdio: 'inherit',
770
- timeout: 120000, // 2 minute timeout
860
+ stdio: 'inherit', // Shows progress output from pnpm
861
+ timeout: 300000, // 5 minute timeout for full install
771
862
  });
772
- console.log(`${PREFIX} ${EMOJI.SUCCESS} pnpm install completed in worktree`);
863
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Worktree dependencies installed`);
773
864
  }
774
865
  catch (installError) {
866
+ // Non-fatal: warn but don't block claim
775
867
  console.warn(`${PREFIX} Warning: pnpm install failed: ${installError.message}`);
776
868
  console.warn(`${PREFIX} You may need to run 'pnpm install' manually in the worktree`);
777
- }
778
- }
779
- // WU-1579: Auto-symlink nested package node_modules for turbo typecheck
780
- // WU-2238: Skip nested symlinks if root symlink was refused (pnpm install handles them)
781
- if (!symlinkResult.refused) {
782
- const nestedResult = symlinkNestedNodeModules(worktreePath, originalCwd);
783
- if (nestedResult.created > 0) {
784
- console.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
869
+ // Fall back to symlink approach so worktree is at least somewhat usable
870
+ console.log(`${PREFIX} Falling back to symlink approach...`);
871
+ applyFallbackSymlinks(worktreePath, originalCwd, console);
785
872
  }
786
873
  }
787
874
  console.log(`${PREFIX} Claim recorded in worktree`);
788
- console.log(`- WU: ${id}${updatedTitle ? ` — ${updatedTitle}` : ''}`);
875
+ console.log(`- WU: ${id}${finalTitle ? ` — ${finalTitle}` : ''}`);
789
876
  console.log(`- Lane: ${args.lane}`);
790
877
  console.log(`- Worktree: ${worktreePath}`);
791
878
  console.log(`- Branch: ${branch}`);
@@ -816,9 +903,44 @@ async function claimWorktreeMode(ctx) {
816
903
  const wuDoc = parseYAML(wuContent);
817
904
  const codePaths = wuDoc.code_paths || [];
818
905
  emitMandatoryAgentAdvisory(codePaths, id);
906
+ // WU-1047: Emit agent-only project defaults from config
907
+ const config = getConfig();
908
+ printProjectDefaults(config?.agents?.methodology);
819
909
  // WU-1763: Print lifecycle nudge with tips for tool adoption
820
910
  printLifecycleNudge(id);
821
911
  }
912
+ /**
913
+ * WU-1047: Format Project Defaults section (agent-only).
914
+ *
915
+ * @param {object} methodology - Methodology defaults config
916
+ * @returns {string} Formatted output or empty string if disabled
917
+ */
918
+ export function formatProjectDefaults(methodology) {
919
+ if (!methodology || methodology.enabled === false)
920
+ return '';
921
+ const enforcement = methodology.enforcement || 'required';
922
+ const principles = Array.isArray(methodology.principles) ? methodology.principles : [];
923
+ const lines = [
924
+ `${PREFIX} 🧭 Project Defaults (agent-only)`,
925
+ ` Enforcement: ${enforcement}`,
926
+ ` Principles: ${principles.length > 0 ? principles.join(', ') : 'None'}`,
927
+ ];
928
+ if (methodology.notes) {
929
+ lines.push(` Notes: ${methodology.notes}`);
930
+ }
931
+ return `\n${lines.join('\n')}`;
932
+ }
933
+ /**
934
+ * WU-1047: Print Project Defaults section (agent-only).
935
+ *
936
+ * @param {object} methodology - Methodology defaults config
937
+ */
938
+ export function printProjectDefaults(methodology) {
939
+ const output = formatProjectDefaults(methodology);
940
+ if (output) {
941
+ console.log(output);
942
+ }
943
+ }
822
944
  /**
823
945
  * WU-1763: Print a single concise tips line to improve tool adoption.
824
946
  * Non-blocking, single-line output to avoid flooding the console.
@@ -829,6 +951,27 @@ export function printLifecycleNudge(_id) {
829
951
  // Single line, concise, actionable
830
952
  console.log(`\n${PREFIX} 💡 Tip: pnpm session:recommend for context tier, mem:ready for pending work, pnpm file:*/git:* for audited wrappers`);
831
953
  }
954
+ /**
955
+ * WU-1029: Apply symlink fallback (root + nested node_modules) after install failure.
956
+ *
957
+ * @param {string} worktreePath - Worktree path
958
+ * @param {string} mainRepoPath - Main repo path
959
+ * @param {Console} logger - Logger (console-compatible)
960
+ */
961
+ export function applyFallbackSymlinks(worktreePath, mainRepoPath, logger = console) {
962
+ const symlinkResult = symlinkNodeModules(worktreePath, logger, mainRepoPath);
963
+ if (symlinkResult.created) {
964
+ logger.log(`${PREFIX} ${EMOJI.SUCCESS} node_modules symlinked as fallback`);
965
+ }
966
+ let nestedResult = null;
967
+ if (!symlinkResult.refused) {
968
+ nestedResult = symlinkNestedNodeModules(worktreePath, mainRepoPath);
969
+ if (nestedResult.created > 0) {
970
+ logger.log(`${PREFIX} ${EMOJI.SUCCESS} ${nestedResult.created} nested node_modules symlinked for typecheck`);
971
+ }
972
+ }
973
+ return { symlinkResult, nestedResult };
974
+ }
832
975
  /**
833
976
  * WU-2411: Handle --resume flag for agent handoff
834
977
  *
@@ -918,6 +1061,8 @@ async function main() {
918
1061
  WU_OPTIONS.reason,
919
1062
  WU_OPTIONS.allowIncomplete,
920
1063
  WU_OPTIONS.resume, // WU-2411: Agent handoff flag
1064
+ WU_OPTIONS.skipSetup, // WU-1023: Skip auto-setup for fast claims
1065
+ WU_OPTIONS.noPush, // Skip pushing claim state/branch (air-gapped)
921
1066
  ],
922
1067
  required: ['id', 'lane'],
923
1068
  allowPositionalId: true,
@@ -943,9 +1088,19 @@ async function main() {
943
1088
  ` 3. Use --no-auto if you already staged claim edits manually`);
944
1089
  }
945
1090
  }
946
- // WU-1361: Fetch and pull FIRST - validate on fresh data, not stale pre-pull data
947
- await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
948
- await getGitForCwd().pull(REMOTES.ORIGIN, BRANCHES.MAIN);
1091
+ let stagedChanges = [];
1092
+ if (args.noAuto) {
1093
+ await ensureCleanOrClaimOnlyWhenNoAuto();
1094
+ stagedChanges = await getStagedChanges();
1095
+ }
1096
+ // WU-1361: Fetch latest remote before validation (no local main mutation)
1097
+ if (!args.noPush) {
1098
+ await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
1099
+ await ensureMainUpToDate(getGitForCwd(), 'wu:claim');
1100
+ }
1101
+ else {
1102
+ console.warn(`${PREFIX} Warning: --no-push enabled. Skipping origin/main sync; local state may be stale.`);
1103
+ }
949
1104
  const WU_PATH = WU_PATHS.WU(id);
950
1105
  const STATUS_PATH = WU_PATHS.STATUS();
951
1106
  const BACKLOG_PATH = WU_PATHS.BACKLOG();
@@ -1039,7 +1194,18 @@ async function main() {
1039
1194
  if (args.branchOnly) {
1040
1195
  await validateBranchOnlyMode(STATUS_PATH, id);
1041
1196
  }
1042
- // Check if branch already exists (prevents duplicate claims)
1197
+ // Check if remote branch already exists (prevents duplicate global claims)
1198
+ if (!args.noPush) {
1199
+ const remoteExists = await getGitForCwd().remoteBranchExists(REMOTES.ORIGIN, branch);
1200
+ if (remoteExists) {
1201
+ die(`Remote branch ${REMOTES.ORIGIN}/${branch} already exists. WU may already be claimed.\n\n` +
1202
+ `Options:\n` +
1203
+ ` 1. Coordinate with the owning agent or wait for completion\n` +
1204
+ ` 2. Choose a different WU\n` +
1205
+ ` 3. Use --no-push for local-only claims (offline)`);
1206
+ }
1207
+ }
1208
+ // Check if branch already exists locally (prevents duplicate claims)
1043
1209
  const branchAlreadyExists = await getGitForCwd().branchExists(branch);
1044
1210
  if (branchAlreadyExists) {
1045
1211
  die(`Branch ${branch} already exists. WU may already be claimed.\n\n` +
@@ -1064,8 +1230,27 @@ async function main() {
1064
1230
  `Manual cleanup: rm -rf ${absoluteWorktreePath}`);
1065
1231
  }
1066
1232
  }
1233
+ // WU-1438: Start agent session BEFORE metadata update to include session_id in YAML
1234
+ let sessionId = null;
1235
+ try {
1236
+ const sessionResult = await startSessionForWU({
1237
+ wuId: id,
1238
+ tier: 2,
1239
+ });
1240
+ sessionId = sessionResult.sessionId;
1241
+ if (sessionResult.alreadyActive) {
1242
+ console.log(`${PREFIX} Agent session already active (${sessionId.slice(0, 8)}...)`);
1243
+ }
1244
+ else {
1245
+ console.log(`${PREFIX} ${EMOJI.SUCCESS} Agent session started (${sessionId.slice(0, 8)}...)`);
1246
+ }
1247
+ }
1248
+ catch (err) {
1249
+ // Non-blocking: session start failure should not block claim
1250
+ console.warn(`${PREFIX} Warning: Could not start agent session: ${err.message}`);
1251
+ }
1067
1252
  // Execute claim workflow
1068
- const ctx = {
1253
+ const baseCtx = {
1069
1254
  args,
1070
1255
  id,
1071
1256
  laneK,
@@ -1077,6 +1262,18 @@ async function main() {
1077
1262
  BACKLOG_PATH,
1078
1263
  claimedMode,
1079
1264
  fixableIssues, // WU-1361: Pass fixable issues for worktree application
1265
+ stagedChanges,
1266
+ };
1267
+ let updatedTitle = title;
1268
+ if (!args.noPush) {
1269
+ updatedTitle = (await applyCanonicalClaimUpdate(baseCtx, sessionId)) || updatedTitle;
1270
+ // Refresh origin/main after push-only update so worktrees start from canonical state
1271
+ await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
1272
+ }
1273
+ const ctx = {
1274
+ ...baseCtx,
1275
+ sessionId,
1276
+ updatedTitle,
1080
1277
  };
1081
1278
  if (args.branchOnly) {
1082
1279
  await claimBranchOnlyMode(ctx);