@lumenflow/cli 3.17.6 → 3.17.7
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/init-docs-scaffolder.js +96 -0
- package/dist/init-docs-scaffolder.js.map +1 -0
- package/dist/init-package-config.js +135 -0
- package/dist/init-package-config.js.map +1 -0
- package/dist/init-safety-scripts.js +129 -0
- package/dist/init-safety-scripts.js.map +1 -0
- package/dist/init.js +13 -302
- package/dist/init.js.map +1 -1
- package/dist/onboarding-template-paths.js +0 -1
- package/dist/onboarding-template-paths.js.map +1 -1
- package/dist/wu-done.js +389 -423
- package/dist/wu-done.js.map +1 -1
- package/package.json +8 -8
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/sidekick/vitest.config.ts +11 -0
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/packs/software-delivery/vitest.config.ts +11 -0
- package/templates/core/LUMENFLOW.md.template +2 -2
- package/templates/core/ai/onboarding/wu-sizing-guide.md.template +0 -84
package/dist/wu-done.js
CHANGED
|
@@ -265,29 +265,9 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
|
|
|
265
265
|
` pnpm wu:done --id ${id}\n\n` +
|
|
266
266
|
`See: https://lumenflow.dev/reference/troubleshooting-wu-done/ for more recovery options.`);
|
|
267
267
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const eventsPath = path.join(resolvedWorktreePath, resolveWuEventsRelativePath(resolvedWorktreePath));
|
|
272
|
-
const store = new WUStateStore(stateDir);
|
|
273
|
-
try {
|
|
274
|
-
await store.load();
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
die(`Cannot read WU state store for ${id}.\n\n` +
|
|
278
|
-
`Path: ${eventsPath}\n\n` +
|
|
279
|
-
`Error: ${getErrorMessage(err)}\n\n` +
|
|
280
|
-
`If this WU was claimed on an older tool version or the event log is missing/corrupt,\n` +
|
|
281
|
-
`repair the worktree state store before rerunning wu:done.`);
|
|
282
|
-
}
|
|
283
|
-
const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
|
|
284
|
-
if (!inProgress.has(id)) {
|
|
285
|
-
die(`WU ${id} is not in_progress in the worktree state store.\n\n` +
|
|
286
|
-
`Path: ${eventsPath}\n\n` +
|
|
287
|
-
`This will fail later when wu:done tries to append a complete event and regenerate backlog/status.\n` +
|
|
288
|
-
`Fix the claim/state log first, then rerun wu:done.`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
268
|
+
// _assertWorktreeWUInProgressInStateStore removed (WU-2400): dead code,
|
|
269
|
+
// strict subset of validateClaimMetadataBeforeGates which already covers
|
|
270
|
+
// YAML status + state store checks with actionable repair guidance.
|
|
291
271
|
/**
|
|
292
272
|
* WU-1946: Update spawn registry on WU completion.
|
|
293
273
|
* Non-blocking wrapper - failures logged as warnings.
|
|
@@ -354,9 +334,13 @@ function getCommitHeaderLimit() {
|
|
|
354
334
|
}
|
|
355
335
|
}
|
|
356
336
|
// ensureOnMain() moved to wu-helpers.ts (WU-1256)
|
|
357
|
-
|
|
337
|
+
/**
|
|
338
|
+
* WU-2400: Generic audit entry appender — shared by auditSkipGates and auditSkipCosGates.
|
|
339
|
+
* Consolidates the duplicate git-identity lookup, JSON serialisation, and file-append logic.
|
|
340
|
+
*/
|
|
341
|
+
async function appendAuditEntry(filename, buildEntry, worktreePath, options) {
|
|
358
342
|
const auditBaseDir = worktreePath || process.cwd();
|
|
359
|
-
const auditPath = path.join(auditBaseDir, '.lumenflow',
|
|
343
|
+
const auditPath = path.join(auditBaseDir, '.lumenflow', filename);
|
|
360
344
|
const auditDir = path.dirname(auditPath);
|
|
361
345
|
if (!existsSync(auditDir))
|
|
362
346
|
mkdirSync(auditDir, { recursive: true });
|
|
@@ -364,18 +348,26 @@ async function auditSkipGates(id, reason, fixWU, worktreePath) {
|
|
|
364
348
|
const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
|
|
365
349
|
const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
|
|
366
350
|
const commitHash = await gitAdapter.getCommitHash();
|
|
367
|
-
const entry =
|
|
368
|
-
id,
|
|
369
|
-
reason,
|
|
370
|
-
fixWU,
|
|
371
|
-
worktreePath,
|
|
351
|
+
const entry = buildEntry({
|
|
372
352
|
userName: userName.trim(),
|
|
373
353
|
userEmail: userEmail.trim(),
|
|
374
354
|
commitHash: commitHash.trim(),
|
|
375
355
|
});
|
|
376
356
|
const line = JSON.stringify(entry);
|
|
377
357
|
appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
378
|
-
|
|
358
|
+
const label = options?.logLabel ?? `Audit event logged to`;
|
|
359
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} ${label} ${path.relative(process.cwd(), auditPath) || auditPath}`);
|
|
360
|
+
}
|
|
361
|
+
async function auditSkipGates(id, reason, fixWU, worktreePath) {
|
|
362
|
+
await appendAuditEntry(SKIP_GATES_AUDIT_FILENAME, (git) => buildSkipGatesAuditEntry({
|
|
363
|
+
id,
|
|
364
|
+
reason,
|
|
365
|
+
fixWU,
|
|
366
|
+
worktreePath,
|
|
367
|
+
userName: git.userName,
|
|
368
|
+
userEmail: git.userEmail,
|
|
369
|
+
commitHash: git.commitHash,
|
|
370
|
+
}), worktreePath, { logLabel: 'Skip-gates event logged to' });
|
|
379
371
|
}
|
|
380
372
|
export function buildSkipGatesAuditEntry(input) {
|
|
381
373
|
const reasonText = typeof input.reason === 'string' ? input.reason : undefined;
|
|
@@ -394,28 +386,17 @@ export function buildSkipGatesAuditEntry(input) {
|
|
|
394
386
|
/**
|
|
395
387
|
* Audit trail for COS gates skip (COS v1.3 S7)
|
|
396
388
|
* WU-1852: Renamed from skip-cos-gates to avoid referencing non-existent CLI flag
|
|
389
|
+
* WU-2400: Delegates to appendAuditEntry to remove DRY violation
|
|
397
390
|
*/
|
|
398
391
|
async function auditSkipCosGates(id, reason, worktreePath) {
|
|
399
|
-
const auditBaseDir = worktreePath || process.cwd();
|
|
400
|
-
const auditPath = path.join(auditBaseDir, '.lumenflow', 'skip-cos-gates-audit.log');
|
|
401
|
-
const auditDir = path.dirname(auditPath);
|
|
402
|
-
if (!existsSync(auditDir))
|
|
403
|
-
mkdirSync(auditDir, { recursive: true });
|
|
404
|
-
const gitAdapter = getGitForCwd();
|
|
405
|
-
const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
|
|
406
|
-
const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
|
|
407
|
-
const commitHash = await gitAdapter.getCommitHash();
|
|
408
392
|
const reasonText = typeof reason === 'string' ? reason : undefined;
|
|
409
|
-
|
|
393
|
+
await appendAuditEntry('skip-cos-gates-audit.log', (git) => ({
|
|
410
394
|
timestamp: new Date().toISOString(),
|
|
411
395
|
wu_id: id,
|
|
412
396
|
reason: reasonText || DEFAULT_NO_REASON,
|
|
413
|
-
git_user: `${userName
|
|
414
|
-
git_commit: commitHash
|
|
415
|
-
};
|
|
416
|
-
const line = JSON.stringify(entry);
|
|
417
|
-
appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
418
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-COS-gates event logged to ${auditPath}`);
|
|
397
|
+
git_user: `${git.userName} <${git.userEmail}>`,
|
|
398
|
+
git_commit: git.commitHash,
|
|
399
|
+
}), worktreePath, { logLabel: 'Skip-COS-gates event logged to' });
|
|
419
400
|
}
|
|
420
401
|
// WU-2308: validateAllPreCommitHooks moved to wu-done-validators.ts
|
|
421
402
|
// Now accepts worktreePath parameter to run audit from worktree context
|
|
@@ -470,20 +451,10 @@ function recordTransactionState(id, wuPath, stampPath, backlogPath, statusPath)
|
|
|
470
451
|
* @param {string} backlogPath - Path to backlog.md (WU-1230)
|
|
471
452
|
* @param {string} statusPath - Path to status.md (WU-1230)
|
|
472
453
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// Previous order (restore → git checkout) caused issues:
|
|
477
|
-
// - git checkout -- . would UNDO file restorations
|
|
478
|
-
// - Left messy state with staged + unstaged conflicts
|
|
479
|
-
//
|
|
480
|
-
// New order:
|
|
481
|
-
// 1. Unstage everything (git reset HEAD)
|
|
482
|
-
// 2. Discard working tree changes (git checkout -- .)
|
|
483
|
-
// 3. Remove stamp if created
|
|
484
|
-
// 4. THEN restore files from txState
|
|
454
|
+
// ── WU-2400: Named rollback sub-operations extracted from rollbackTransaction ──
|
|
455
|
+
/** Unstage all staged files and discard working tree changes. */
|
|
456
|
+
async function resetGitStaging() {
|
|
485
457
|
// Step 1: Unstage all staged files FIRST
|
|
486
|
-
// Emergency fix Session 2: Use git-adapter instead of raw execSync
|
|
487
458
|
try {
|
|
488
459
|
const gitAdapter = getGitForCwd();
|
|
489
460
|
await gitAdapter.raw(['reset', 'HEAD']);
|
|
@@ -493,7 +464,6 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
|
|
|
493
464
|
// Ignore - may not have anything staged
|
|
494
465
|
}
|
|
495
466
|
// Step 2: Discard working directory changes (reset to last commit)
|
|
496
|
-
// Emergency fix Session 2: Use git-adapter instead of raw execSync
|
|
497
467
|
try {
|
|
498
468
|
const gitAdapter = getGitForCwd();
|
|
499
469
|
await gitAdapter.raw(['checkout', '--', '.']);
|
|
@@ -502,43 +472,42 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
|
|
|
502
472
|
catch {
|
|
503
473
|
// Ignore - may not have anything to discard
|
|
504
474
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (existsSync(stampPath))
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
475
|
+
}
|
|
476
|
+
/** Remove stamp file unconditionally if it exists (WU-1440). */
|
|
477
|
+
function removeStampIfExists(stampPath) {
|
|
478
|
+
if (!existsSync(stampPath))
|
|
479
|
+
return;
|
|
480
|
+
try {
|
|
481
|
+
unlinkSync(stampPath);
|
|
482
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed ${stampPath}`);
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to remove stamp: ${getErrorMessage(err)}`);
|
|
516
486
|
}
|
|
517
|
-
|
|
518
|
-
|
|
487
|
+
}
|
|
488
|
+
/** Build file list and restore from transaction snapshot (WU-1255). */
|
|
489
|
+
function restoreFilesFromSnapshot(txState, wuPath, backlogPath, statusPath) {
|
|
519
490
|
const filesToRestore = [];
|
|
520
|
-
// Restore backlog.md (ref: WU-1230)
|
|
521
491
|
if (txState.backlogContent && existsSync(backlogPath)) {
|
|
522
492
|
filesToRestore.push({ name: 'backlog.md', path: backlogPath, content: txState.backlogContent });
|
|
523
493
|
}
|
|
524
|
-
// Restore status.md (ref: WU-1230)
|
|
525
494
|
if (txState.statusContent && existsSync(statusPath)) {
|
|
526
495
|
filesToRestore.push({ name: 'status.md', path: statusPath, content: txState.statusContent });
|
|
527
496
|
}
|
|
528
|
-
// Restore WU YAML if it was modified
|
|
529
497
|
if (txState.wuYamlContent && existsSync(wuPath)) {
|
|
530
498
|
filesToRestore.push({ name: 'WU YAML', path: wuPath, content: txState.wuYamlContent });
|
|
531
499
|
}
|
|
532
|
-
// WU-1255: Use rollbackFiles utility for per-file error tracking
|
|
533
500
|
const restoreResult = rollbackFiles(filesToRestore);
|
|
534
|
-
// Log results
|
|
535
501
|
for (const name of restoreResult.restored) {
|
|
536
502
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Restored ${name}`);
|
|
537
503
|
}
|
|
538
504
|
for (const err of restoreResult.errors) {
|
|
539
505
|
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to restore ${err.name}: ${err.error}`);
|
|
540
506
|
}
|
|
541
|
-
|
|
507
|
+
return restoreResult;
|
|
508
|
+
}
|
|
509
|
+
/** Reset main branch to original SHA if we drifted during the transaction. */
|
|
510
|
+
async function resetMainBranchIfNeeded(txState) {
|
|
542
511
|
try {
|
|
543
512
|
const gitAdapter = getGitForCwd();
|
|
544
513
|
const currentBranch = await gitAdapter.getCurrentBranch();
|
|
@@ -546,7 +515,6 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
|
|
|
546
515
|
const currentSHA = await gitAdapter.getCommitHash();
|
|
547
516
|
if (currentSHA !== txState.mainSHA) {
|
|
548
517
|
await gitAdapter.reset(txState.mainSHA, { hard: true });
|
|
549
|
-
// Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
|
|
550
518
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset main to ${txState.mainSHA.slice(0, GIT.SHA_SHORT_LENGTH)}`);
|
|
551
519
|
}
|
|
552
520
|
}
|
|
@@ -554,9 +522,9 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
|
|
|
554
522
|
catch (e) {
|
|
555
523
|
console.warn(`${LOG_PREFIX.DONE} Warning: Could not reset main: ${getErrorMessage(e)}`);
|
|
556
524
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
525
|
+
}
|
|
526
|
+
/** WU-1280: Verify clean git status after rollback and log any residue. */
|
|
527
|
+
async function verifyCleanGitStateAfterRollback() {
|
|
560
528
|
try {
|
|
561
529
|
const gitAdapter = getGitForCwd();
|
|
562
530
|
const statusOutput = (await gitAdapter.raw(['status', '--porcelain'])).trim();
|
|
@@ -570,6 +538,17 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
|
|
|
570
538
|
catch {
|
|
571
539
|
// Ignore - git status may fail in edge cases
|
|
572
540
|
}
|
|
541
|
+
}
|
|
542
|
+
// ── End of extracted rollback sub-operations ──
|
|
543
|
+
async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
|
|
544
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
|
|
545
|
+
// WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
|
|
546
|
+
// WU-2400: Delegates to named sub-operations for readability.
|
|
547
|
+
await resetGitStaging();
|
|
548
|
+
removeStampIfExists(stampPath);
|
|
549
|
+
const restoreResult = restoreFilesFromSnapshot(txState, wuPath, backlogPath, statusPath);
|
|
550
|
+
await resetMainBranchIfNeeded(txState);
|
|
551
|
+
await verifyCleanGitStateAfterRollback();
|
|
573
552
|
// WU-1255: Report final status with all errors
|
|
574
553
|
if (restoreResult.errors.length > 0) {
|
|
575
554
|
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback completed with ${restoreResult.errors.length} error(s):`);
|
|
@@ -650,9 +629,9 @@ function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
|
|
|
650
629
|
* @param {string|null} params.derivedWorktree - Derived worktree path
|
|
651
630
|
* @returns {Promise<{title: string, docForValidation: object}>} Updated title and doc
|
|
652
631
|
*/
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
632
|
+
// ── WU-2400: Named validator functions extracted from executePreFlightChecks ──
|
|
633
|
+
/** Validate WU YAML against Zod schema and done-specific rules. */
|
|
634
|
+
function preflightValidateYamlSchema(docForValidation) {
|
|
656
635
|
console.log(`${LOG_PREFIX.DONE} Validating WU YAML structure...`);
|
|
657
636
|
const schemaResult = validateWU(docForValidation);
|
|
658
637
|
if (!schemaResult.success) {
|
|
@@ -661,7 +640,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
661
640
|
.join(STRING_LITERALS.NEWLINE);
|
|
662
641
|
die(`❌ WU YAML validation failed:\n\n${errors}\n\nFix these issues before running wu:done`);
|
|
663
642
|
}
|
|
664
|
-
// Additional done-specific validation
|
|
665
643
|
if (docForValidation.status === WU_STATUS.DONE) {
|
|
666
644
|
const doneResult = validateDoneWU(schemaResult.data);
|
|
667
645
|
if (!doneResult.valid) {
|
|
@@ -669,10 +647,12 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
669
647
|
}
|
|
670
648
|
}
|
|
671
649
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML validation passed`);
|
|
672
|
-
|
|
673
|
-
|
|
650
|
+
return schemaResult;
|
|
651
|
+
}
|
|
652
|
+
/** WU-2079: Ensure required approvals are present before allowing completion. */
|
|
653
|
+
function preflightValidateApprovalGates(id, schemaData) {
|
|
674
654
|
console.log(`${LOG_PREFIX.DONE} Checking approval gates...`);
|
|
675
|
-
const approvalResult = validateApprovalGates(
|
|
655
|
+
const approvalResult = validateApprovalGates(schemaData);
|
|
676
656
|
if (!approvalResult.valid) {
|
|
677
657
|
const governancePath = getConfig({ projectRoot: process.cwd() }).directories.governancePath;
|
|
678
658
|
die(`❌ Approval gates not satisfied:\n\n${approvalResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}\n\n` +
|
|
@@ -682,15 +662,16 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
682
662
|
` 3. Re-run: pnpm wu:done --id ${id}\n\n` +
|
|
683
663
|
` See ${governancePath} for role definitions.`);
|
|
684
664
|
}
|
|
685
|
-
// Log advisory warnings (non-blocking)
|
|
686
665
|
if (approvalResult.warnings.length > 0) {
|
|
687
666
|
approvalResult.warnings.forEach((w) => {
|
|
688
667
|
console.warn(`${LOG_PREFIX.DONE} ⚠️ ${w}`);
|
|
689
668
|
});
|
|
690
669
|
}
|
|
691
670
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Approval gates passed`);
|
|
671
|
+
}
|
|
672
|
+
/** WU-1805 + WU-2310: Validate code_paths consistency (preflight + type vs code_paths). */
|
|
673
|
+
async function preflightValidateCodePathsConsistency(id, docForValidation, derivedWorktree) {
|
|
692
674
|
// WU-1805: Preflight code_paths and test_paths validation
|
|
693
|
-
// Run BEFORE gates to catch YAML mismatches early (saves time vs. discovering after full gate run)
|
|
694
675
|
const preflightResult = await executePreflightCodePathValidation(id, {
|
|
695
676
|
rootDir: process.cwd(),
|
|
696
677
|
worktreePath: derivedWorktree,
|
|
@@ -706,7 +687,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
706
687
|
}
|
|
707
688
|
}
|
|
708
689
|
// WU-2310: Preflight type vs code_paths validation
|
|
709
|
-
// Run BEFORE transaction to prevent documentation WUs with code paths from failing at git commit
|
|
710
690
|
console.log(`${LOG_PREFIX.DONE} Validating type vs code_paths (WU-2310)...`);
|
|
711
691
|
const typeVsCodePathsResult = validateTypeVsCodePathsPreflight(docForValidation);
|
|
712
692
|
if (!typeVsCodePathsResult.valid) {
|
|
@@ -714,10 +694,9 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
714
694
|
die(errorMessage);
|
|
715
695
|
}
|
|
716
696
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Type vs code_paths validation passed`);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
// Fail fast if WU is in both Done and In Progress sections
|
|
697
|
+
}
|
|
698
|
+
/** WU-1234 + WU-1276: Validate backlog and WU state store consistency. */
|
|
699
|
+
async function preflightValidateBacklogAndStateConsistency(id) {
|
|
721
700
|
console.log(`${LOG_PREFIX.DONE} Checking backlog consistency...`);
|
|
722
701
|
const backlogPath = WU_PATHS.BACKLOG();
|
|
723
702
|
const backlogConsistency = checkBacklogConsistencyForWU(id, backlogPath);
|
|
@@ -725,8 +704,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
725
704
|
die(backlogConsistency.error ?? 'Backlog consistency check failed');
|
|
726
705
|
}
|
|
727
706
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Backlog consistency check passed`);
|
|
728
|
-
// WU-1276: Pre-flight WU state consistency check
|
|
729
|
-
// Layer 2 defense-in-depth: fail fast if WU has pre-existing inconsistencies
|
|
730
707
|
console.log(`${LOG_PREFIX.DONE} Checking WU state consistency...`);
|
|
731
708
|
const stateCheck = await checkWUConsistency(id);
|
|
732
709
|
if (!stateCheck.valid) {
|
|
@@ -737,7 +714,10 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
737
714
|
`Fix with: pnpm wu:repair --id ${id}`);
|
|
738
715
|
}
|
|
739
716
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU state consistency check passed`);
|
|
740
|
-
|
|
717
|
+
}
|
|
718
|
+
/** Validate worktree state: branch-only vs worktree mode, parallel detection, claim metadata. */
|
|
719
|
+
async function preflightValidateWorktreeState(params) {
|
|
720
|
+
const { id, args, isBranchOnly, docMain, docForValidation, derivedWorktree } = params;
|
|
741
721
|
if (isBranchOnly) {
|
|
742
722
|
const laneBranch = await defaultBranchFrom(docMain);
|
|
743
723
|
if (!laneBranch)
|
|
@@ -748,193 +728,162 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
748
728
|
}
|
|
749
729
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Branch-Only mode validation passed`);
|
|
750
730
|
console.log(`${LOG_PREFIX.DONE} Working on branch: ${laneBranch}`);
|
|
731
|
+
return;
|
|
751
732
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
let uncommittedBriefLines = [];
|
|
766
|
-
try {
|
|
767
|
-
const preRestoreContent = readFileSync(wuEventsAbsPath, {
|
|
768
|
-
encoding: FILE_SYSTEM.UTF8,
|
|
769
|
-
});
|
|
770
|
-
const committedContent = (() => {
|
|
771
|
-
try {
|
|
772
|
-
return execSync(`git -C "${derivedWorktree}" show HEAD:"${wuEventsRelPath}"`, {
|
|
773
|
-
encoding: FILE_SYSTEM.UTF8,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
return '';
|
|
778
|
-
}
|
|
779
|
-
})();
|
|
780
|
-
const committedLines = new Set(committedContent.trim().split('\n').filter(Boolean));
|
|
781
|
-
uncommittedBriefLines = preRestoreContent
|
|
782
|
-
.trim()
|
|
783
|
-
.split('\n')
|
|
784
|
-
.filter((line) => !committedLines.has(line) && line.includes('[wu:brief]'));
|
|
785
|
-
}
|
|
786
|
-
catch {
|
|
787
|
-
// Non-fatal: file might not exist
|
|
788
|
-
}
|
|
789
|
-
try {
|
|
790
|
-
execSync(`git -C "${derivedWorktree}" restore "${wuEventsRelPath}"`);
|
|
791
|
-
}
|
|
792
|
-
catch {
|
|
793
|
-
// Non-fatal: file might not exist or already clean
|
|
794
|
-
}
|
|
795
|
-
// Re-append preserved wu:brief evidence lines
|
|
796
|
-
if (uncommittedBriefLines.length > 0) {
|
|
733
|
+
// Worktree mode: must be on main
|
|
734
|
+
await ensureOnMain(getGitForCwd());
|
|
735
|
+
// P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
|
|
736
|
+
// WU-2370: Preserve wu:brief evidence lines across the restore.
|
|
737
|
+
if (derivedWorktree) {
|
|
738
|
+
const wuEventsRelPath = resolveWuEventsRelativePath(derivedWorktree);
|
|
739
|
+
const wuEventsAbsPath = path.join(derivedWorktree, wuEventsRelPath);
|
|
740
|
+
let uncommittedBriefLines = [];
|
|
741
|
+
try {
|
|
742
|
+
const preRestoreContent = readFileSync(wuEventsAbsPath, {
|
|
743
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
744
|
+
});
|
|
745
|
+
const committedContent = (() => {
|
|
797
746
|
try {
|
|
798
|
-
|
|
747
|
+
return execSync(`git -C "${derivedWorktree}" show HEAD:"${wuEventsRelPath}"`, {
|
|
799
748
|
encoding: FILE_SYSTEM.UTF8,
|
|
800
749
|
});
|
|
801
750
|
}
|
|
802
751
|
catch {
|
|
803
|
-
|
|
752
|
+
return '';
|
|
804
753
|
}
|
|
805
|
-
}
|
|
754
|
+
})();
|
|
755
|
+
const committedLines = new Set(committedContent.trim().split('\n').filter(Boolean));
|
|
756
|
+
uncommittedBriefLines = preRestoreContent
|
|
757
|
+
.trim()
|
|
758
|
+
.split('\n')
|
|
759
|
+
.filter((line) => !committedLines.has(line) && line.includes('[wu:brief]'));
|
|
806
760
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// WU-1588: Check inbox for recent signals from parallel agents
|
|
822
|
-
// Non-blocking: failures handled internally by checkInboxForRecentSignals
|
|
823
|
-
await checkInboxForRecentSignals(id);
|
|
824
|
-
// WU-1584: Instead of proceeding with warning, trigger auto-rebase
|
|
825
|
-
// This prevents merge conflicts that would fail downstream
|
|
826
|
-
if (derivedWorktree && !args.noAutoRebase) {
|
|
827
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
|
|
828
|
-
const laneBranch = await defaultBranchFrom(docForValidation);
|
|
829
|
-
if (laneBranch) {
|
|
830
|
-
const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
|
|
831
|
-
if (rebaseResult.success) {
|
|
832
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
|
|
833
|
-
emitTelemetry({
|
|
834
|
-
script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
|
|
835
|
-
wu_id: id,
|
|
836
|
-
step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
|
|
837
|
-
parallel_wus: parallelResult.completedWUs,
|
|
838
|
-
count: parallelResult.completedWUs.length,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
// Rebase failed - provide detailed instructions
|
|
843
|
-
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
|
|
844
|
-
console.error(rebaseResult.error);
|
|
845
|
-
die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
|
|
846
|
-
`Manual resolution required - see instructions above.`);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
761
|
+
catch {
|
|
762
|
+
// Non-fatal: file might not exist
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
execSync(`git -C "${derivedWorktree}" restore "${wuEventsRelPath}"`);
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// Non-fatal: file might not exist or already clean
|
|
769
|
+
}
|
|
770
|
+
if (uncommittedBriefLines.length > 0) {
|
|
771
|
+
try {
|
|
772
|
+
appendFileSync(wuEventsAbsPath, uncommittedBriefLines.join('\n') + '\n', {
|
|
773
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
774
|
+
});
|
|
849
775
|
}
|
|
850
|
-
|
|
851
|
-
//
|
|
852
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
|
|
776
|
+
catch {
|
|
777
|
+
// Non-fatal: best-effort evidence preservation
|
|
853
778
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// WU-1382: Detect parallel completions and warn
|
|
782
|
+
// WU-1584 Fix #3: Trigger auto-rebase instead of just warning
|
|
783
|
+
console.log(`${LOG_PREFIX.DONE} Checking for parallel WU completions...`);
|
|
784
|
+
const parallelResult = await detectParallelCompletions(id, docForValidation);
|
|
785
|
+
if (parallelResult.hasParallelCompletions) {
|
|
786
|
+
console.warn(parallelResult.warning);
|
|
787
|
+
emitTelemetry({
|
|
788
|
+
script: 'wu-done',
|
|
789
|
+
wu_id: id,
|
|
790
|
+
step: 'parallel_detection',
|
|
791
|
+
parallel_wus: parallelResult.completedWUs,
|
|
792
|
+
count: parallelResult.completedWUs.length,
|
|
793
|
+
});
|
|
794
|
+
await checkInboxForRecentSignals(id);
|
|
795
|
+
if (derivedWorktree && !args.noAutoRebase) {
|
|
796
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
|
|
797
|
+
const laneBranch = await defaultBranchFrom(docForValidation);
|
|
798
|
+
if (laneBranch) {
|
|
799
|
+
const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
|
|
800
|
+
if (rebaseResult.success) {
|
|
801
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
|
|
802
|
+
emitTelemetry({
|
|
803
|
+
script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
|
|
804
|
+
wu_id: id,
|
|
805
|
+
step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
|
|
806
|
+
parallel_wus: parallelResult.completedWUs,
|
|
807
|
+
count: parallelResult.completedWUs.length,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
|
|
812
|
+
console.error(rebaseResult.error);
|
|
813
|
+
die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
|
|
814
|
+
`Manual resolution required - see instructions above.`);
|
|
815
|
+
}
|
|
857
816
|
}
|
|
858
817
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (derivedWorktree) {
|
|
862
|
-
await runBackgroundProcessCheck(derivedWorktree);
|
|
818
|
+
else if (!args.noAutoRebase) {
|
|
819
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
|
|
863
820
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// Provides actionable guidance to run wu:repair --claim if validation fails.
|
|
867
|
-
if (derivedWorktree) {
|
|
868
|
-
await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
|
|
821
|
+
else {
|
|
822
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase disabled (--no-auto-rebase) - proceeding with caution`);
|
|
869
823
|
}
|
|
870
824
|
}
|
|
871
|
-
//
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
console.log('\n📝 Docs-only WU detected');
|
|
875
|
-
console.log(' - Gates will skip lint/typecheck/tests');
|
|
876
|
-
console.log(' - Only docs/markdown paths allowed\n');
|
|
825
|
+
// WU-1381: Detect background processes that might interfere with gates
|
|
826
|
+
if (derivedWorktree) {
|
|
827
|
+
await runBackgroundProcessCheck(derivedWorktree);
|
|
877
828
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
829
|
+
// WU-1804: Fail fast before gates with comprehensive claim metadata check.
|
|
830
|
+
if (derivedWorktree) {
|
|
831
|
+
await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/** Validate ownership: session ownership + assigned_to ownership (worktree mode only). */
|
|
835
|
+
async function preflightValidateOwnership(params) {
|
|
836
|
+
const { id, args, isBranchOnly, docForValidation, derivedWorktree } = params;
|
|
837
|
+
if (isBranchOnly)
|
|
838
|
+
return;
|
|
839
|
+
const activeSession = getCurrentSessionForWU();
|
|
840
|
+
const prepCheckpointResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
|
|
841
|
+
const sessionOwnership = validateClaimSessionOwnership({
|
|
842
|
+
wuId: id,
|
|
843
|
+
claimedSessionId: typeof docForValidation.session_id === 'string' ? docForValidation.session_id : null,
|
|
844
|
+
activeSessionId: activeSession?.session_id ?? null,
|
|
845
|
+
force: Boolean(args.force),
|
|
846
|
+
hasValidPrepCheckpoint: prepCheckpointResult.canSkip,
|
|
847
|
+
skipGates: Boolean(args['skip-gates']),
|
|
848
|
+
});
|
|
849
|
+
if (!sessionOwnership.valid) {
|
|
850
|
+
die(sessionOwnership.error ?? 'Claim-session ownership check failed');
|
|
882
851
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const prepCheckpointResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
|
|
888
|
-
const sessionOwnership = validateClaimSessionOwnership({
|
|
852
|
+
if (sessionOwnership.auditRequired &&
|
|
853
|
+
typeof docForValidation.session_id === 'string' &&
|
|
854
|
+
derivedWorktree) {
|
|
855
|
+
await appendClaimSessionOverrideAuditEvent({
|
|
889
856
|
wuId: id,
|
|
890
|
-
claimedSessionId:
|
|
857
|
+
claimedSessionId: docForValidation.session_id,
|
|
891
858
|
activeSessionId: activeSession?.session_id ?? null,
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
skipGates: Boolean(args['skip-gates']),
|
|
859
|
+
reason: args.reason || 'force ownership override',
|
|
860
|
+
worktreePath: derivedWorktree,
|
|
895
861
|
});
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
|
|
912
|
-
if (!ownershipCheck.valid) {
|
|
913
|
-
die(ownershipCheck.error ?? 'Ownership check failed');
|
|
914
|
-
}
|
|
915
|
-
// If override was used, log to audit trail and add to WU notes
|
|
916
|
-
if (ownershipCheck.auditEntry) {
|
|
917
|
-
auditOwnershipOverride(ownershipCheck.auditEntry);
|
|
918
|
-
// Add override reason to WU notes (schema requires string, not array)
|
|
919
|
-
const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
|
|
920
|
-
appendNote(docForValidation, overrideNote);
|
|
921
|
-
// Write updated WU YAML back to worktree
|
|
922
|
-
if (derivedWorktree) {
|
|
923
|
-
const wtWUPath = path.join(derivedWorktree, WU_PATHS.WU(id));
|
|
924
|
-
if (existsSync(wtWUPath)) {
|
|
925
|
-
writeWU(wtWUPath, docForValidation);
|
|
926
|
-
}
|
|
862
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Claim-session ownership overridden with --force; audit checkpoint recorded.`);
|
|
863
|
+
}
|
|
864
|
+
const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
|
|
865
|
+
if (!ownershipCheck.valid) {
|
|
866
|
+
die(ownershipCheck.error ?? 'Ownership check failed');
|
|
867
|
+
}
|
|
868
|
+
if (ownershipCheck.auditEntry) {
|
|
869
|
+
auditOwnershipOverride(ownershipCheck.auditEntry);
|
|
870
|
+
const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
|
|
871
|
+
appendNote(docForValidation, overrideNote);
|
|
872
|
+
if (derivedWorktree) {
|
|
873
|
+
const wtWUPath = path.join(derivedWorktree, WU_PATHS.WU(id));
|
|
874
|
+
if (existsSync(wtWUPath)) {
|
|
875
|
+
writeWU(wtWUPath, docForValidation);
|
|
927
876
|
}
|
|
928
877
|
}
|
|
929
878
|
}
|
|
930
|
-
|
|
931
|
-
|
|
879
|
+
}
|
|
880
|
+
/** WU-1280: Early spec completeness validation (before gates). */
|
|
881
|
+
function preflightValidateSpecCompleteness(id, docForValidation) {
|
|
932
882
|
console.log(`\n${LOG_PREFIX.DONE} Validating spec completeness for ${id}...`);
|
|
933
883
|
const specResult = validateSpecCompleteness(docForValidation, id);
|
|
934
884
|
if (!specResult.valid) {
|
|
935
885
|
console.error(`\n❌ Spec completeness validation failed for ${id}:\n`);
|
|
936
886
|
specResult.errors.forEach((err) => console.error(` - ${err}`));
|
|
937
|
-
// WU-1311: Use config-based path in error message
|
|
938
887
|
const specConfig = getConfig();
|
|
939
888
|
console.error(`\nFix these issues before running wu:done:\n` +
|
|
940
889
|
` 1. Update ${specConfig.directories.wuDir}/${id}.yaml\n` +
|
|
@@ -947,33 +896,18 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
947
896
|
die(`Cannot mark ${id} as done - spec incomplete`);
|
|
948
897
|
}
|
|
949
898
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spec completeness check passed`);
|
|
950
|
-
// WU-1351: Validate code_paths files exist
|
|
951
|
-
// In worktree mode: validate files exist in worktree (will be merged)
|
|
952
|
-
// In branch-only mode: validate files exist on current branch
|
|
899
|
+
// WU-1351: Validate code_paths files exist
|
|
953
900
|
console.log(`\n${LOG_PREFIX.DONE} Validating code_paths existence for ${id}...`);
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
});
|
|
958
|
-
if ('valid' in codePathsResult && !codePathsResult.valid) {
|
|
959
|
-
console.error(`\n❌ code_paths validation failed for ${id}:\n`);
|
|
960
|
-
if ('errors' in codePathsResult) {
|
|
961
|
-
codePathsResult.errors.forEach((err) => console.error(err));
|
|
962
|
-
}
|
|
963
|
-
die(`Cannot mark ${id} as done - code_paths missing from target branch`);
|
|
964
|
-
}
|
|
965
|
-
// WU-1324 + WU-1542: Check mandatory agent compliance
|
|
966
|
-
// WU-1542: --require-agents makes this a BLOCKING check
|
|
967
|
-
const codePaths = docForValidation.code_paths || [];
|
|
901
|
+
}
|
|
902
|
+
/** WU-1324 + WU-1542: Check mandatory agent compliance. */
|
|
903
|
+
function preflightCheckMandatoryAgents(id, args, codePaths) {
|
|
968
904
|
const compliance = checkMandatoryAgentsComplianceBlocking(codePaths, id, {
|
|
969
905
|
blocking: Boolean(args.requireAgents),
|
|
970
906
|
});
|
|
971
907
|
if (compliance.blocking && compliance.errorMessage) {
|
|
972
|
-
// WU-1542: Blocking mode - fail wu:done with detailed error
|
|
973
908
|
die(compliance.errorMessage);
|
|
974
909
|
}
|
|
975
910
|
else if (!compliance.compliant) {
|
|
976
|
-
// Non-blocking mode - show warning (original WU-1324 behavior)
|
|
977
911
|
console.warn(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} MANDATORY AGENT WARNING`);
|
|
978
912
|
console.warn(`The following mandatory agents were not confirmed as invoked:`);
|
|
979
913
|
for (const agent of compliance.missing) {
|
|
@@ -982,6 +916,72 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
982
916
|
console.warn(`\nThis is a NON-BLOCKING warning.`);
|
|
983
917
|
console.warn(`Use --require-agents to make this a blocking error.\n`);
|
|
984
918
|
}
|
|
919
|
+
}
|
|
920
|
+
/** Validate --skip-gates requirements: --reason and --fix-wu are mandatory. */
|
|
921
|
+
function preflightValidateSkipGatesRequirements(args) {
|
|
922
|
+
if (!args.skipGates)
|
|
923
|
+
return;
|
|
924
|
+
if (!args.reason) {
|
|
925
|
+
die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
|
|
926
|
+
}
|
|
927
|
+
if (!args.fixWu) {
|
|
928
|
+
die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
|
|
929
|
+
}
|
|
930
|
+
if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
|
|
931
|
+
die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
// ── End of extracted validators ──
|
|
935
|
+
async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docMain, docForValidation, derivedWorktree, }) {
|
|
936
|
+
// WU-2400: Delegates to named validator functions to reduce cognitive complexity.
|
|
937
|
+
const schemaResult = preflightValidateYamlSchema(docForValidation);
|
|
938
|
+
// schemaResult.data is guaranteed defined: preflightValidateYamlSchema die()s on failure
|
|
939
|
+
preflightValidateApprovalGates(id, schemaResult.data);
|
|
940
|
+
await preflightValidateCodePathsConsistency(id, docForValidation, derivedWorktree);
|
|
941
|
+
// Tripwire: Scan commands log for violations
|
|
942
|
+
runTripwireCheck();
|
|
943
|
+
await preflightValidateBacklogAndStateConsistency(id);
|
|
944
|
+
await preflightValidateWorktreeState({
|
|
945
|
+
id,
|
|
946
|
+
args,
|
|
947
|
+
isBranchOnly,
|
|
948
|
+
docMain,
|
|
949
|
+
docForValidation,
|
|
950
|
+
derivedWorktree,
|
|
951
|
+
});
|
|
952
|
+
// Use worktree title for commit message (not stale main title)
|
|
953
|
+
const title = docForValidation.title || docMain.title || '';
|
|
954
|
+
if (isDocsOnly) {
|
|
955
|
+
console.log('\n📝 Docs-only WU detected');
|
|
956
|
+
console.log(' - Gates will skip lint/typecheck/tests');
|
|
957
|
+
console.log(' - Only docs/markdown paths allowed\n');
|
|
958
|
+
}
|
|
959
|
+
if (isBranchOnly) {
|
|
960
|
+
console.log('\n🌿 Branch-Only mode detected');
|
|
961
|
+
console.log(' - Gates run in main checkout on lane branch');
|
|
962
|
+
console.log(' - No worktree to remove\n');
|
|
963
|
+
}
|
|
964
|
+
await preflightValidateOwnership({
|
|
965
|
+
id,
|
|
966
|
+
args,
|
|
967
|
+
isBranchOnly,
|
|
968
|
+
docForValidation,
|
|
969
|
+
derivedWorktree,
|
|
970
|
+
});
|
|
971
|
+
preflightValidateSpecCompleteness(id, docForValidation);
|
|
972
|
+
// code_paths existence check (continuation of spec completeness)
|
|
973
|
+
const codePathsResult = await validateCodePathsExist(docForValidation, id, {
|
|
974
|
+
worktreePath: derivedWorktree,
|
|
975
|
+
targetBranch: isBranchOnly ? 'HEAD' : BRANCHES.MAIN,
|
|
976
|
+
});
|
|
977
|
+
if ('valid' in codePathsResult && !codePathsResult.valid) {
|
|
978
|
+
console.error(`\n❌ code_paths validation failed for ${id}:\n`);
|
|
979
|
+
if ('errors' in codePathsResult) {
|
|
980
|
+
codePathsResult.errors.forEach((err) => console.error(err));
|
|
981
|
+
}
|
|
982
|
+
die(`Cannot mark ${id} as done - code_paths missing from target branch`);
|
|
983
|
+
}
|
|
984
|
+
preflightCheckMandatoryAgents(id, args, docForValidation.code_paths || []);
|
|
985
985
|
// WU-1012: Validate --docs-only flag usage (BLOCKING)
|
|
986
986
|
const docsOnlyValidation = validateDocsOnlyFlag(docForValidation, { docsOnly: args.docsOnly });
|
|
987
987
|
if (!docsOnlyValidation.valid) {
|
|
@@ -995,18 +995,7 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
|
|
|
995
995
|
});
|
|
996
996
|
// Run WU validator
|
|
997
997
|
runWUValidator(docForValidation, id, args.allowTodo, derivedWorktree);
|
|
998
|
-
|
|
999
|
-
if (args.skipGates) {
|
|
1000
|
-
if (!args.reason) {
|
|
1001
|
-
die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
|
|
1002
|
-
}
|
|
1003
|
-
if (!args.fixWu) {
|
|
1004
|
-
die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
|
|
1005
|
-
}
|
|
1006
|
-
if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
|
|
1007
|
-
die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
998
|
+
preflightValidateSkipGatesRequirements(args);
|
|
1010
999
|
return { title, docForValidation };
|
|
1011
1000
|
}
|
|
1012
1001
|
/**
|
|
@@ -1041,101 +1030,73 @@ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree,
|
|
|
1041
1030
|
const worktreeDisplay = isBranchOnly ? 'none' : derivedWorktree || 'none';
|
|
1042
1031
|
console.log(`\n${LOG_PREFIX.DONE} HUD: WU=${id} status=${yamlStatus} stamp=${stampExists} locked=${yamlLocked} mode=${mode} branch=${branch} worktree=${worktreeDisplay}`);
|
|
1043
1032
|
}
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
//
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
die(`${EMOJI.FAILURE}
|
|
1056
|
-
|
|
1057
|
-
`
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1033
|
+
// ── WU-2400: Extracted path-specific handlers from main() ──
|
|
1034
|
+
/**
|
|
1035
|
+
* WU-2211: Handle --already-merged early exit path.
|
|
1036
|
+
* Skips merge phase, gates, worktree detection. Only writes metadata.
|
|
1037
|
+
*/
|
|
1038
|
+
async function executeAlreadyMergedFinalizePath(id, docMain) {
|
|
1039
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2211: --already-merged mode activated`);
|
|
1040
|
+
// Safety check: verify code_paths exist on HEAD of main
|
|
1041
|
+
const codePaths = docMain.code_paths || [];
|
|
1042
|
+
const verification = await verifyCodePathsOnMainHead(codePaths);
|
|
1043
|
+
if (!verification.valid) {
|
|
1044
|
+
die(`${EMOJI.FAILURE} --already-merged safety check failed\n\n` +
|
|
1045
|
+
`${verification.error}\n\n` +
|
|
1046
|
+
`Cannot finalize ${id}: code_paths must exist on HEAD before using --already-merged.`);
|
|
1047
|
+
}
|
|
1048
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Safety check passed: all ${codePaths.length} code_paths verified on HEAD`);
|
|
1049
|
+
// Execute finalize-only path
|
|
1050
|
+
const title = String(docMain.title || id);
|
|
1051
|
+
const lane = String(docMain.lane || '');
|
|
1052
|
+
const finalizeResult = await executeAlreadyMergedFinalizeFromModule({
|
|
1053
|
+
id,
|
|
1054
|
+
title,
|
|
1055
|
+
lane,
|
|
1056
|
+
doc: docMain,
|
|
1057
|
+
});
|
|
1058
|
+
if (!finalizeResult.success) {
|
|
1059
|
+
die(`${EMOJI.FAILURE} --already-merged finalization failed\n\n` +
|
|
1060
|
+
`Errors:\n${finalizeResult.errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
|
|
1061
|
+
`Partial state may remain. Rerun: pnpm wu:done --id ${id} --already-merged`);
|
|
1063
1062
|
}
|
|
1064
|
-
//
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// Capture main checkout path once. process.cwd() may drift later during recovery flows.
|
|
1072
|
-
const mainCheckoutPath = process.cwd();
|
|
1073
|
-
// ──────────────────────────────────────────────
|
|
1074
|
-
// WU-2211: --already-merged early exit path
|
|
1075
|
-
// Skips merge phase, gates, worktree detection. Only writes metadata.
|
|
1076
|
-
// ──────────────────────────────────────────────
|
|
1077
|
-
if (args.alreadyMerged) {
|
|
1078
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2211: --already-merged mode activated`);
|
|
1079
|
-
// Safety check: verify code_paths exist on HEAD of main
|
|
1080
|
-
const codePaths = docMain.code_paths || [];
|
|
1081
|
-
const verification = await verifyCodePathsOnMainHead(codePaths);
|
|
1082
|
-
if (!verification.valid) {
|
|
1083
|
-
die(`${EMOJI.FAILURE} --already-merged safety check failed\n\n` +
|
|
1084
|
-
`${verification.error}\n\n` +
|
|
1085
|
-
`Cannot finalize ${id}: code_paths must exist on HEAD before using --already-merged.`);
|
|
1086
|
-
}
|
|
1087
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Safety check passed: all ${codePaths.length} code_paths verified on HEAD`);
|
|
1088
|
-
// Execute finalize-only path
|
|
1089
|
-
const title = String(docMain.title || id);
|
|
1090
|
-
const lane = String(docMain.lane || '');
|
|
1091
|
-
const finalizeResult = await executeAlreadyMergedFinalizeFromModule({
|
|
1092
|
-
id,
|
|
1093
|
-
title,
|
|
1094
|
-
lane,
|
|
1095
|
-
doc: docMain,
|
|
1096
|
-
});
|
|
1097
|
-
if (!finalizeResult.success) {
|
|
1098
|
-
die(`${EMOJI.FAILURE} --already-merged finalization failed\n\n` +
|
|
1099
|
-
`Errors:\n${finalizeResult.errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
|
|
1100
|
-
`Partial state may remain. Rerun: pnpm wu:done --id ${id} --already-merged`);
|
|
1101
|
-
}
|
|
1102
|
-
// Release lane lock (non-blocking, same as normal wu:done)
|
|
1103
|
-
try {
|
|
1104
|
-
const lane = docMain.lane;
|
|
1105
|
-
if (lane) {
|
|
1106
|
-
const releaseResult = releaseLaneLock(lane, { wuId: id });
|
|
1107
|
-
if (releaseResult.released && !releaseResult.notFound) {
|
|
1108
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
|
|
1109
|
-
}
|
|
1063
|
+
// Release lane lock (non-blocking)
|
|
1064
|
+
try {
|
|
1065
|
+
const lane = docMain.lane;
|
|
1066
|
+
if (lane) {
|
|
1067
|
+
const releaseResult = releaseLaneLock(lane, { wuId: id });
|
|
1068
|
+
if (releaseResult.released && !releaseResult.notFound) {
|
|
1069
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
|
|
1110
1070
|
}
|
|
1111
1071
|
}
|
|
1112
|
-
catch (err) {
|
|
1113
|
-
console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
|
|
1114
|
-
}
|
|
1115
|
-
// End agent session (non-blocking)
|
|
1116
|
-
try {
|
|
1117
|
-
endSessionForWU();
|
|
1118
|
-
}
|
|
1119
|
-
catch {
|
|
1120
|
-
// Non-blocking
|
|
1121
|
-
}
|
|
1122
|
-
// Broadcast completion signal (non-blocking)
|
|
1123
|
-
await broadcastCompletionSignal(id, title);
|
|
1124
|
-
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${id} finalized via --already-merged`);
|
|
1125
|
-
console.log(`- WU: ${id} -- ${title}`);
|
|
1126
|
-
clearConfigCache();
|
|
1127
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
1128
1072
|
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
|
|
1075
|
+
}
|
|
1076
|
+
// End agent session (non-blocking)
|
|
1077
|
+
try {
|
|
1078
|
+
endSessionForWU();
|
|
1079
|
+
}
|
|
1080
|
+
catch {
|
|
1081
|
+
// Non-blocking
|
|
1082
|
+
}
|
|
1083
|
+
// Broadcast completion signal (non-blocking)
|
|
1084
|
+
await broadcastCompletionSignal(id, title);
|
|
1085
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${id} finalized via --already-merged`);
|
|
1086
|
+
console.log(`- WU: ${id} -- ${title}`);
|
|
1087
|
+
clearConfigCache();
|
|
1088
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* WU-2400: The normal wu:done pipeline (validation, gates, completion, cleanup).
|
|
1092
|
+
* Extracted from main() to reduce cognitive complexity.
|
|
1093
|
+
*/
|
|
1094
|
+
async function executeNormalWuDonePath(params) {
|
|
1095
|
+
const { id, args, docMain, initialDocForValidation, isBranchOnly, isBranchPR, derivedWorktree, isDocsOnly, mainCheckoutPath, WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, } = params;
|
|
1129
1096
|
// WU-1663: Determine prepPassed early for pipeline actor input.
|
|
1130
|
-
// canSkipGates checks if wu:prep already ran gates successfully via checkpoint.
|
|
1131
|
-
// This drives the isPrepPassed guard on the GATES_SKIPPED transition.
|
|
1132
|
-
// WU-2102: Look for checkpoint in worktree (where wu:prep writes it), not main
|
|
1133
1097
|
const earlySkipResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
|
|
1134
1098
|
const prepPassed = earlySkipResult.canSkip;
|
|
1135
1099
|
// WU-1663: Create XState pipeline actor for state-driven orchestration.
|
|
1136
|
-
// The actor tracks which pipeline stage we're in (validating, gating, committing, etc.)
|
|
1137
|
-
// and provides explicit state/transition contracts. Existing procedural logic continues
|
|
1138
|
-
// to do the real work; the actor provides structured state tracking alongside it.
|
|
1139
1100
|
const pipelineActor = createActor(wuDoneMachine, {
|
|
1140
1101
|
input: {
|
|
1141
1102
|
wuId: id,
|
|
@@ -1144,7 +1105,6 @@ export async function main() {
|
|
|
1144
1105
|
},
|
|
1145
1106
|
});
|
|
1146
1107
|
pipelineActor.start();
|
|
1147
|
-
// WU-1663: Send START event to transition from idle -> validating
|
|
1148
1108
|
pipelineActor.send({
|
|
1149
1109
|
type: WU_DONE_EVENTS.START,
|
|
1150
1110
|
wuId: id,
|
|
@@ -1159,7 +1119,6 @@ export async function main() {
|
|
|
1159
1119
|
: null;
|
|
1160
1120
|
const worktreeExists = resolvedWorktreePath ? existsSync(resolvedWorktreePath) : false;
|
|
1161
1121
|
const { allowFallback: allowBranchOnlyFallback, effectiveBranchOnly } = computeBranchOnlyFallback({
|
|
1162
|
-
// WU-1590: Treat branch-pr like branch-only for fallback computation
|
|
1163
1122
|
isBranchOnly: isNoWorktreeMode,
|
|
1164
1123
|
branchOnlyRequested: args.branchOnly,
|
|
1165
1124
|
worktreeExists,
|
|
@@ -1181,18 +1140,15 @@ export async function main() {
|
|
|
1181
1140
|
die(mainMutationGuard.message ?? 'wu:done blocked by dirty-main guard.');
|
|
1182
1141
|
}
|
|
1183
1142
|
// WU-2327: Verify current-session wu:brief evidence before pre-flight restores
|
|
1184
|
-
// the tracked worktree wu-events log used to keep auto-rebase safe.
|
|
1185
1143
|
await enforceWuBriefEvidenceForDone(id, docMain, {
|
|
1186
1144
|
baseDir: effectiveWorktreePath || mainCheckoutPath,
|
|
1187
1145
|
force: Boolean(args.force),
|
|
1188
1146
|
});
|
|
1189
1147
|
// WU-1169: Ensure worktree is clean before proceeding
|
|
1190
|
-
// This prevents WU-1943 rollback loops if rebase fails due to dirty state
|
|
1191
1148
|
if (effectiveWorktreePath && existsSync(effectiveWorktreePath)) {
|
|
1192
1149
|
await ensureCleanWorktree(effectiveWorktreePath);
|
|
1193
1150
|
}
|
|
1194
|
-
// Pre-flight checks
|
|
1195
|
-
// WU-1663: Wrap in try/catch to send pipeline failure event before die() propagates
|
|
1151
|
+
// Pre-flight checks
|
|
1196
1152
|
let preFlightResult;
|
|
1197
1153
|
try {
|
|
1198
1154
|
preFlightResult = await executePreFlightChecks({
|
|
@@ -1214,18 +1170,15 @@ export async function main() {
|
|
|
1214
1170
|
throw preFlightErr;
|
|
1215
1171
|
}
|
|
1216
1172
|
const title = preFlightResult.title;
|
|
1217
|
-
// Note: docForValidation is returned but not used after pre-flight checks
|
|
1218
|
-
// The metadata transaction uses docForUpdate instead
|
|
1219
|
-
// WU-1663: Pre-flight checks passed - transition to preparing state
|
|
1220
1173
|
pipelineActor.send({ type: WU_DONE_EVENTS.VALIDATION_PASSED });
|
|
1221
1174
|
// WU-1599: Enforce auditable spawn provenance for initiative-governed WUs.
|
|
1222
1175
|
await enforceSpawnProvenanceForDone(id, docMain, {
|
|
1223
1176
|
baseDir: mainCheckoutPath,
|
|
1224
1177
|
force: Boolean(args.force),
|
|
1225
1178
|
});
|
|
1226
|
-
// Step 0: Run gates
|
|
1179
|
+
// Step 0: Run gates
|
|
1227
1180
|
const worktreePath = effectiveWorktreePath;
|
|
1228
|
-
// WU-1471 AC3 + WU-1998: Config-driven checkpoint gate
|
|
1181
|
+
// WU-1471 AC3 + WU-1998: Config-driven checkpoint gate
|
|
1229
1182
|
const checkpointGateConfig = getConfig();
|
|
1230
1183
|
const requireCheckpoint = resolveCheckpointGateMode(checkpointGateConfig.memory?.enforcement?.require_checkpoint_for_done);
|
|
1231
1184
|
await enforceCheckpointGateForDone({
|
|
@@ -1233,13 +1186,11 @@ export async function main() {
|
|
|
1233
1186
|
workspacePath: worktreePath || mainCheckoutPath,
|
|
1234
1187
|
mode: requireCheckpoint,
|
|
1235
1188
|
});
|
|
1236
|
-
// WU-1663: Preparation complete - transition to gating state
|
|
1237
1189
|
pipelineActor.send({ type: WU_DONE_EVENTS.PREPARATION_COMPLETE });
|
|
1238
1190
|
// WU-2102: Resolve scoped test paths from WU spec tests.unit for gate fallback
|
|
1239
1191
|
const scopedTestPathsForDone = resolveScopedUnitTestsForPrep({
|
|
1240
1192
|
tests: docMain.tests,
|
|
1241
1193
|
});
|
|
1242
|
-
// WU-1663: Wrap gates in try/catch to send pipeline failure event
|
|
1243
1194
|
let gateExecutionResult;
|
|
1244
1195
|
try {
|
|
1245
1196
|
gateExecutionResult = await executeGates({
|
|
@@ -1264,15 +1215,12 @@ export async function main() {
|
|
|
1264
1215
|
pipelineActor.stop();
|
|
1265
1216
|
throw gateErr;
|
|
1266
1217
|
}
|
|
1267
|
-
// WU-1663: Gates passed - transition from gating state.
|
|
1268
|
-
// Use GATES_SKIPPED if checkpoint dedup allowed skip, GATES_PASSED otherwise.
|
|
1269
1218
|
if (gateExecutionResult.skippedByCheckpoint) {
|
|
1270
1219
|
pipelineActor.send({ type: WU_DONE_EVENTS.GATES_SKIPPED });
|
|
1271
1220
|
}
|
|
1272
1221
|
else {
|
|
1273
1222
|
pipelineActor.send({ type: WU_DONE_EVENTS.GATES_PASSED });
|
|
1274
1223
|
}
|
|
1275
|
-
// Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
|
|
1276
1224
|
printStateHUD({
|
|
1277
1225
|
id,
|
|
1278
1226
|
docMain,
|
|
@@ -1294,7 +1242,6 @@ export async function main() {
|
|
|
1294
1242
|
runGatesFn: ({ cwd }) => runGates({ cwd, docsOnly: false }),
|
|
1295
1243
|
});
|
|
1296
1244
|
// Step 1: Execute mode-specific completion workflow (WU-2167)
|
|
1297
|
-
// Main remains orchestration-only; execution details live in wu-done-mode-execution.ts.
|
|
1298
1245
|
let completionResult = { cleanupSafe: true };
|
|
1299
1246
|
if (!args.noAuto) {
|
|
1300
1247
|
completionResult = await executeModeSpecificCompletion({
|
|
@@ -1324,9 +1271,7 @@ export async function main() {
|
|
|
1324
1271
|
await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
|
|
1325
1272
|
}
|
|
1326
1273
|
// WU-2262: Do not run repository-wide worktree_path sanitation from local main during wu:done.
|
|
1327
|
-
//
|
|
1328
|
-
// Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
|
|
1329
|
-
// WU-1811: Only run cleanup if all completion steps succeeded
|
|
1274
|
+
// Step 6 & 7: Cleanup (remove worktree, delete branch)
|
|
1330
1275
|
if (completionResult.cleanupSafe !== false) {
|
|
1331
1276
|
await runCleanup(docMain, args);
|
|
1332
1277
|
}
|
|
@@ -1334,7 +1279,6 @@ export async function main() {
|
|
|
1334
1279
|
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Skipping worktree cleanup - metadata/push incomplete`);
|
|
1335
1280
|
}
|
|
1336
1281
|
// WU-1603: Release lane lock after successful completion
|
|
1337
|
-
// This allows the lane to be claimed by another WU
|
|
1338
1282
|
try {
|
|
1339
1283
|
const lane = docMain.lane;
|
|
1340
1284
|
if (lane) {
|
|
@@ -1342,11 +1286,9 @@ export async function main() {
|
|
|
1342
1286
|
if (releaseResult.released && !releaseResult.notFound) {
|
|
1343
1287
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
|
|
1344
1288
|
}
|
|
1345
|
-
// Silent if notFound - lock may not exist (older WUs, manual cleanup)
|
|
1346
1289
|
}
|
|
1347
1290
|
}
|
|
1348
1291
|
catch (err) {
|
|
1349
|
-
// Non-blocking: lock release failure should not block completion
|
|
1350
1292
|
console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
|
|
1351
1293
|
}
|
|
1352
1294
|
// WU-1438: Auto-end agent session
|
|
@@ -1355,28 +1297,21 @@ export async function main() {
|
|
|
1355
1297
|
if (sessionResult.ended) {
|
|
1356
1298
|
const sessionId = sessionResult.summary?.session_id;
|
|
1357
1299
|
if (sessionId) {
|
|
1358
|
-
// Emergency fix Session 2: Use SESSION.ID_DISPLAY_LENGTH constant
|
|
1359
1300
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Agent session ended (${sessionId.slice(0, SESSION.ID_DISPLAY_LENGTH)}...)`);
|
|
1360
1301
|
}
|
|
1361
1302
|
}
|
|
1362
|
-
// No warning if no active session - silent no-op is expected
|
|
1363
1303
|
}
|
|
1364
1304
|
catch (err) {
|
|
1365
|
-
// Non-blocking: session end failure should not block completion
|
|
1366
1305
|
console.warn(`${LOG_PREFIX.DONE} Warning: Could not end agent session: ${getErrorMessage(err)}`);
|
|
1367
1306
|
}
|
|
1368
1307
|
// WU-1588: Broadcast completion signal after session end
|
|
1369
|
-
// Non-blocking: failures handled internally by broadcastCompletionSignal
|
|
1370
1308
|
await broadcastCompletionSignal(id, title);
|
|
1371
|
-
// WU-1473: Mark completed-WU signals as read
|
|
1372
|
-
// Non-blocking: markCompletedWUSignalsAsRead is fail-open (AC4)
|
|
1309
|
+
// WU-1473: Mark completed-WU signals as read
|
|
1373
1310
|
const markResult = await markCompletedWUSignalsAsRead(mainCheckoutPath, id);
|
|
1374
1311
|
if (markResult.markedCount > 0) {
|
|
1375
1312
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked ${markResult.markedCount} signal(s) as read for ${id}`);
|
|
1376
1313
|
}
|
|
1377
1314
|
// WU-1946: Update spawn registry to mark WU as completed
|
|
1378
|
-
// Non-blocking: failures handled internally by updateSpawnRegistryOnCompletion
|
|
1379
|
-
// Works in both worktree and branch-only modes (called after completionResult)
|
|
1380
1315
|
await updateSpawnRegistryOnCompletion(id, mainCheckoutPath);
|
|
1381
1316
|
await flushWuLifecycleSync({
|
|
1382
1317
|
command: WU_LIFECYCLE_COMMANDS.DONE,
|
|
@@ -1388,13 +1323,10 @@ export async function main() {
|
|
|
1388
1323
|
},
|
|
1389
1324
|
});
|
|
1390
1325
|
// WU-1747: Clear checkpoint on successful completion
|
|
1391
|
-
// Checkpoint is no longer needed once WU is fully complete
|
|
1392
1326
|
clearCheckpoint(id, { baseDir: worktreePath || undefined });
|
|
1393
1327
|
// WU-1471 AC4: Remove per-WU hook counter file on completion
|
|
1394
|
-
// Fail-safe: cleanupHookCounters never throws
|
|
1395
1328
|
cleanupHookCounters(mainCheckoutPath, id);
|
|
1396
1329
|
// WU-1474: Invoke decay archival when memory.decay policy is configured
|
|
1397
|
-
// Non-blocking: errors are captured but never block wu:done completion (fail-open)
|
|
1398
1330
|
try {
|
|
1399
1331
|
const decayConfig = getConfig().memory?.decay;
|
|
1400
1332
|
const decayResult = await runDecayOnDone(mainCheckoutPath, decayConfig);
|
|
@@ -1406,27 +1338,20 @@ export async function main() {
|
|
|
1406
1338
|
}
|
|
1407
1339
|
}
|
|
1408
1340
|
catch (err) {
|
|
1409
|
-
// Double fail-open: even if runDecayOnDone itself throws unexpectedly, never block wu:done
|
|
1410
1341
|
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Decay archival error (fail-open): ${getErrorMessage(err)}`);
|
|
1411
1342
|
}
|
|
1412
1343
|
// WU-1663: Cleanup complete - transition to final done state
|
|
1413
1344
|
pipelineActor.send({ type: WU_DONE_EVENTS.CLEANUP_COMPLETE });
|
|
1414
|
-
// WU-1663: Log final pipeline state for diagnostics
|
|
1415
1345
|
const finalSnapshot = pipelineActor.getSnapshot();
|
|
1416
1346
|
console.log(`${LOG_PREFIX.DONE} Pipeline state: ${finalSnapshot.value} (WU-1663)`);
|
|
1417
1347
|
pipelineActor.stop();
|
|
1418
1348
|
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
|
|
1419
1349
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
|
|
1420
1350
|
console.log(`- WU: ${id} — ${title}`);
|
|
1421
|
-
// WU-2126: Invalidate config cache so subsequent commands in the same process
|
|
1422
|
-
// read fresh values from disk (wu:done may have mutated workspace.yaml/state).
|
|
1423
1351
|
clearConfigCache();
|
|
1424
1352
|
// WU-1763: Print lifecycle nudges (conditional, non-blocking)
|
|
1425
|
-
// Discovery summary nudge - only if discoveries exist
|
|
1426
1353
|
const discoveries = await loadDiscoveriesForWU(mainCheckoutPath, id);
|
|
1427
1354
|
printDiscoveryNudge(id, discoveries.count, discoveries.ids);
|
|
1428
|
-
// Documentation validation nudge - only if docs changed
|
|
1429
|
-
// Use worktreePath if available, otherwise skip (branch-only mode has no worktree)
|
|
1430
1355
|
if (worktreePath) {
|
|
1431
1356
|
const changedDocs = await detectChangedDocPaths(worktreePath, BRANCHES.MAIN);
|
|
1432
1357
|
printDocValidationNudge(id, changedDocs);
|
|
@@ -1436,17 +1361,58 @@ export async function main() {
|
|
|
1436
1361
|
currentBranch !== BRANCHES.MAIN &&
|
|
1437
1362
|
currentBranch !== BRANCHES.MASTER;
|
|
1438
1363
|
if (shouldRunCleanupMutations) {
|
|
1439
|
-
// WU-1366: Auto state cleanup after successful completion
|
|
1440
|
-
// Non-fatal: errors are logged but do not block completion
|
|
1441
1364
|
await runAutoCleanupAfterDone(mainCheckoutPath);
|
|
1442
|
-
// WU-1533: Auto-commit dirty state files left by cleanup.
|
|
1443
|
-
// Branch-aware: in branch-pr mode this stays on the lane branch.
|
|
1444
1365
|
await commitCleanupChanges({ targetBranch: currentBranch });
|
|
1445
1366
|
}
|
|
1446
1367
|
else {
|
|
1447
1368
|
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1611: Skipping auto-cleanup mutations on protected branch ${currentBranch}`);
|
|
1448
1369
|
}
|
|
1449
1370
|
}
|
|
1371
|
+
// ── End of extracted path-specific handlers ──
|
|
1372
|
+
export async function main() {
|
|
1373
|
+
// Allow pre-push hook to recognize wu:done automation (WU-1030)
|
|
1374
|
+
process.env[ENV_VARS.WU_TOOL] = 'wu-done';
|
|
1375
|
+
// Validate CLI arguments and WU ID format
|
|
1376
|
+
const { args, id } = validateInputs(process.argv);
|
|
1377
|
+
// WU-1223: Check if running from worktree - wu:done requires main checkout
|
|
1378
|
+
const { LOCATION_TYPES } = CONTEXT_VALIDATION;
|
|
1379
|
+
const currentLocation = await resolveLocation();
|
|
1380
|
+
if (currentLocation.type === LOCATION_TYPES.WORKTREE) {
|
|
1381
|
+
die(`${EMOJI.FAILURE} wu:done must be run from main checkout, not from a worktree.\n\n` +
|
|
1382
|
+
`Current location: ${currentLocation.cwd}\n\n` +
|
|
1383
|
+
`WU-1223 NEW WORKFLOW:\n` +
|
|
1384
|
+
` 1. From worktree, run: pnpm wu:prep --id ${id}\n` +
|
|
1385
|
+
` (This runs gates and prepares for completion)\n\n` +
|
|
1386
|
+
` 2. From main, run: cd ${currentLocation.mainCheckout} && pnpm wu:done --id ${id}\n` +
|
|
1387
|
+
` (This does merge + cleanup only)\n\n` +
|
|
1388
|
+
`Use wu:prep to run gates in the worktree, then wu:done from main for merge/cleanup.`);
|
|
1389
|
+
}
|
|
1390
|
+
// Detect workspace mode and calculate paths
|
|
1391
|
+
const pathInfo = await detectModeAndPaths(id, args);
|
|
1392
|
+
const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain: docMainRaw, isBranchOnly, isBranchPR, derivedWorktree, docForValidation: initialDocForValidationRaw, isDocsOnly, } = pathInfo;
|
|
1393
|
+
const docMain = normalizeWUDocLike(docMainRaw);
|
|
1394
|
+
const initialDocForValidation = normalizeWUDocLike(initialDocForValidationRaw);
|
|
1395
|
+
const mainCheckoutPath = process.cwd();
|
|
1396
|
+
// WU-2400: Dispatch to extracted path-specific handlers.
|
|
1397
|
+
if (args.alreadyMerged) {
|
|
1398
|
+
await executeAlreadyMergedFinalizePath(id, docMain);
|
|
1399
|
+
}
|
|
1400
|
+
await executeNormalWuDonePath({
|
|
1401
|
+
id,
|
|
1402
|
+
args,
|
|
1403
|
+
docMain,
|
|
1404
|
+
initialDocForValidation,
|
|
1405
|
+
isBranchOnly,
|
|
1406
|
+
isBranchPR,
|
|
1407
|
+
derivedWorktree,
|
|
1408
|
+
isDocsOnly,
|
|
1409
|
+
mainCheckoutPath,
|
|
1410
|
+
WU_PATH,
|
|
1411
|
+
STATUS_PATH,
|
|
1412
|
+
BACKLOG_PATH,
|
|
1413
|
+
STAMPS_DIR,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1450
1416
|
/**
|
|
1451
1417
|
* WU-1763: Print discovery summary nudge when discoveries exist for this WU.
|
|
1452
1418
|
* Conditional output - only prints when discoveryCount > 0.
|