@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/__tests__/flow-report.test.js +24 -0
- package/dist/__tests__/metrics-snapshot.test.js +24 -0
- package/dist/agent-issues-query.js +251 -0
- package/dist/agent-log-issue.js +67 -0
- package/dist/agent-session-end.js +36 -0
- package/dist/agent-session.js +46 -0
- package/dist/flow-bottlenecks.js +183 -0
- package/dist/flow-report.js +311 -0
- package/dist/gates.js +126 -49
- package/dist/init.js +297 -0
- package/dist/initiative-bulk-assign-wus.js +315 -0
- package/dist/initiative-create.js +3 -7
- package/dist/initiative-edit.js +3 -3
- package/dist/metrics-snapshot.js +314 -0
- package/dist/orchestrate-init-status.js +64 -0
- package/dist/orchestrate-initiative.js +100 -0
- package/dist/orchestrate-monitor.js +90 -0
- package/dist/wu-claim.js +313 -116
- package/dist/wu-cleanup.js +49 -3
- package/dist/wu-create.js +195 -121
- package/dist/wu-delete.js +241 -0
- package/dist/wu-done.js +146 -23
- package/dist/wu-edit.js +152 -61
- package/dist/wu-infer-lane.js +2 -2
- package/dist/wu-spawn.js +77 -158
- package/dist/wu-unlock-lane.js +158 -0
- package/package.json +30 -10
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(
|
|
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
|
-
|
|
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
|
|
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}"
|
|
522
|
-
console.warn(`${PREFIX} ⚠️
|
|
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
|
|
527
|
-
`
|
|
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
|
|
624
|
+
` 1. Wait for a WU to complete or block\n` +
|
|
530
625
|
` 2. Choose a different lane\n` +
|
|
531
|
-
` 3.
|
|
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
|
-
//
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}${
|
|
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
|
-
|
|
787
|
+
sessionId, updatedTitle, stagedChanges, } = ctx;
|
|
699
788
|
const originalCwd = process.cwd();
|
|
700
789
|
const worktreePath = path.resolve(worktree);
|
|
701
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
//
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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-
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
|
|
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
|
|
762
|
-
// WU-
|
|
763
|
-
//
|
|
764
|
-
console.log(`${PREFIX}
|
|
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:
|
|
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}
|
|
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
|
-
|
|
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}${
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
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);
|