@lumenflow/core 1.0.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/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validation functions for wu:done workflow
|
|
4
|
+
* Extracted from wu-done.mjs (WU-1215 refactoring)
|
|
5
|
+
*/
|
|
6
|
+
/* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
|
|
7
|
+
import { parseWUArgs } from './arg-parser.js';
|
|
8
|
+
import { die, createError, ErrorCodes } from './error-handler.js';
|
|
9
|
+
import { WU_PATHS } from './wu-paths.js';
|
|
10
|
+
import { EXIT_CODES } from './wu-constants.js';
|
|
11
|
+
// WU-1352: Use centralized YAML functions from wu-yaml.mjs
|
|
12
|
+
import { readWU, writeWU, parseYAML } from './wu-yaml.js';
|
|
13
|
+
import { getGitForCwd } from './git-adapter.js';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { access } from 'node:fs/promises';
|
|
17
|
+
import { exec as execCallback, execSync as execSyncImport } from 'node:child_process';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
19
|
+
import { updateStatusRemoveInProgress, addToStatusCompleted } from './wu-status-updater.js';
|
|
20
|
+
import { moveWUToDoneBacklog } from './wu-backlog-updater.js';
|
|
21
|
+
import { createStamp } from './stamp-utils.js';
|
|
22
|
+
import { WU_EVENTS_FILE_NAME } from './wu-state-store.js';
|
|
23
|
+
import { computeWUYAMLContent, computeStatusContent, computeBacklogContent, computeWUEventsContentAfterComplete, computeStampContent, } from './wu-transaction-collectors.js';
|
|
24
|
+
const execAsync = promisify(execCallback);
|
|
25
|
+
import { PATTERNS, REMOTES, toKebab, VALIDATION, WU_TYPES, TEST_TYPES, DEFAULTS, LOG_PREFIX, EMOJI, CLAIMED_MODES, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, GIT_COMMANDS, BRANCHES, STRING_LITERALS, BEACON_PATHS, STDIO, } from './wu-constants.js';
|
|
26
|
+
import { PLACEHOLDER_SENTINEL } from './wu-schema.js';
|
|
27
|
+
// WU-1433: Manual test escape hatch validator
|
|
28
|
+
import { validateAutomatedTestRequirement } from './manual-test-validator.js';
|
|
29
|
+
// WU-1440: Import merged check for branch deletion
|
|
30
|
+
import { isBranchAlreadyMerged } from './wu-done-worktree.js';
|
|
31
|
+
// WU-2241: Import cleanup lock for concurrent collision prevention
|
|
32
|
+
import { withCleanupLock } from './cleanup-lock.js';
|
|
33
|
+
// WU-1805: Import preflight validators for code_paths validation
|
|
34
|
+
import { validatePreflight } from './wu-preflight-validators.js';
|
|
35
|
+
// WU-2242: Import isDocumentationPath for test_paths enforcement
|
|
36
|
+
import { isDocumentationPath } from './file-classifiers.js';
|
|
37
|
+
// WU-2278: Import ownership validation for cross-agent protection
|
|
38
|
+
import { validateWorktreeOwnership } from './worktree-ownership.js';
|
|
39
|
+
// WU-2278: Import cleanup install config for timeout and CI mode
|
|
40
|
+
import { getCleanupInstallConfig, CLEANUP_INSTALL_TIMEOUT_MS } from './cleanup-install-config.js';
|
|
41
|
+
/**
|
|
42
|
+
* Prefixes for paths that qualify as "docs-only" (no code changes).
|
|
43
|
+
* Unlike SKIP_TESTS_PREFIXES, this excludes tools/ and scripts/ because
|
|
44
|
+
* those contain code files that require full gate validation.
|
|
45
|
+
*
|
|
46
|
+
* WU-1539: Split from shouldSkipWebTests to fix docs-only misclassification.
|
|
47
|
+
* @constant {string[]}
|
|
48
|
+
*/
|
|
49
|
+
const DOCS_ONLY_PREFIXES = Object.freeze(['docs/', 'ai/', '.claude/', 'memory-bank/']);
|
|
50
|
+
/**
|
|
51
|
+
* Root file patterns that qualify as docs-only.
|
|
52
|
+
* @constant {string[]}
|
|
53
|
+
*/
|
|
54
|
+
const DOCS_ONLY_ROOT_FILES = Object.freeze(['readme', 'claude']);
|
|
55
|
+
/**
|
|
56
|
+
* WU-1234 + WU-1255 + WU-1539: Detect docs-only WU from code_paths
|
|
57
|
+
* Returns true if all code_paths are documentation paths only.
|
|
58
|
+
*
|
|
59
|
+
* Docs-only paths: docs/, ai/, .claude/, memory-bank/, README*, CLAUDE*.md
|
|
60
|
+
* NOT docs-only: tools/, scripts/ (these are code, not documentation)
|
|
61
|
+
*
|
|
62
|
+
* WU-1539: Fixed misclassification where tools/ was treated as docs-only
|
|
63
|
+
* but then rejected by validateDocsOnly(). tools/ should skip web tests
|
|
64
|
+
* but NOT be classified as docs-only.
|
|
65
|
+
*
|
|
66
|
+
* @param {string[]|null|undefined} codePaths - Array of file paths from WU YAML
|
|
67
|
+
* @returns {boolean} True if WU is docs-only (all paths are documentation)
|
|
68
|
+
*/
|
|
69
|
+
function detectDocsOnlyByPaths(codePaths) {
|
|
70
|
+
if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return codePaths.every((filePath) => {
|
|
74
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const path = filePath.trim();
|
|
78
|
+
if (path.length === 0) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// Check docs-only prefixes (docs/, ai/, .claude/, memory-bank/)
|
|
82
|
+
for (const prefix of DOCS_ONLY_PREFIXES) {
|
|
83
|
+
if (path.startsWith(prefix)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Check if it's a markdown file (*.md)
|
|
88
|
+
if (path.endsWith('.md')) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// Check root file patterns (README*, CLAUDE*.md)
|
|
92
|
+
const lowerPath = path.toLowerCase();
|
|
93
|
+
for (const pattern of DOCS_ONLY_ROOT_FILES) {
|
|
94
|
+
if (lowerPath.startsWith(pattern)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validates command-line inputs and WU ID format
|
|
103
|
+
* @param {string[]} argv - Process arguments
|
|
104
|
+
* @returns {{ args: object, id: string }} Parsed args and validated WU ID
|
|
105
|
+
*/
|
|
106
|
+
export function validateInputs(argv) {
|
|
107
|
+
const args = parseWUArgs(argv);
|
|
108
|
+
if (args.help || !args.id) {
|
|
109
|
+
console.log('Usage: pnpm wu:done --id WU-334 [OPTIONS]\n\n' +
|
|
110
|
+
'Options:\n' +
|
|
111
|
+
' --worktree <path> Override worktree path (default: worktrees/<lane>-<wu-id>)\n' +
|
|
112
|
+
' --no-auto Skip auto-updating YAML/backlog/status (you staged manually)\n' +
|
|
113
|
+
' --no-remove Skip worktree removal\n' +
|
|
114
|
+
' --no-merge Skip auto-merging lane branch to main\n' +
|
|
115
|
+
' --delete-branch Delete lane branch after merge (both local and remote)\n' +
|
|
116
|
+
' --create-pr Create PR instead of auto-merge (requires gh CLI)\n' +
|
|
117
|
+
' --pr-draft Create PR as draft (use with --create-pr)\n' +
|
|
118
|
+
' --skip-gates Skip gates check (USE WITH EXTREME CAUTION)\n' +
|
|
119
|
+
' --reason "<text>" Required with --skip-gates or --override-owner\n' +
|
|
120
|
+
' --fix-wu WU-{id} Required with --skip-gates: WU ID that will fix the failures\n' +
|
|
121
|
+
' --allow-todo Allow TODO comments in code (requires justification in WU notes)\n' +
|
|
122
|
+
' --override-owner Override ownership check (requires --reason, audited)\n' +
|
|
123
|
+
' --no-auto-rebase Disable auto-rebase on branch divergence (WU-1303)\n' +
|
|
124
|
+
' --require-agents Block completion if mandatory agents not invoked (WU-1542)\n' +
|
|
125
|
+
' --help, -h Show this help\n\n' +
|
|
126
|
+
'⚠️ SKIP-GATES WARNING:\n' +
|
|
127
|
+
' Only use --skip-gates when:\n' +
|
|
128
|
+
' • Test failures are confirmed pre-existing (not introduced by your WU)\n' +
|
|
129
|
+
' • A separate WU exists to fix those failures (specify with --fix-wu)\n' +
|
|
130
|
+
' • Your WU work is genuinely complete\n\n' +
|
|
131
|
+
' NEVER use --skip-gates for failures introduced by your WU!\n' +
|
|
132
|
+
' All skip-gates events are logged to .beacon/skip-gates-audit.log\n\n' +
|
|
133
|
+
'📝 WU VALIDATOR:\n' +
|
|
134
|
+
' Automatically scans code_paths for:\n' +
|
|
135
|
+
' • TODO/FIXME/HACK/XXX comments (fails validation unless --allow-todo)\n' +
|
|
136
|
+
' • Mock/Stub/Fake classes in production code (warning only)\n' +
|
|
137
|
+
' Use --allow-todo only for legitimate cases with justification in WU notes.\n');
|
|
138
|
+
process.exit(args.help ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
139
|
+
}
|
|
140
|
+
const id = args.id.toUpperCase();
|
|
141
|
+
if (!PATTERNS.WU_ID.test(id))
|
|
142
|
+
die(`Invalid WU id '${args.id}'. Expected format WU-123`);
|
|
143
|
+
return { args, id };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read WU YAML preferring worktree version over main version
|
|
147
|
+
*
|
|
148
|
+
* WU-1584 Fix #4: Added diagnostic logging to confirm which YAML file is being
|
|
149
|
+
* read for code_paths validation. This helps debug issues where worktree YAML
|
|
150
|
+
* differs from main checkout YAML.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} id - WU ID
|
|
153
|
+
* @param {string|null} worktreePath - Worktree path (null if branch-only mode)
|
|
154
|
+
* @param {string} mainWUPath - Path to WU YAML in main checkout
|
|
155
|
+
* @returns {object} Parsed WU document
|
|
156
|
+
*/
|
|
157
|
+
export function readWUPreferWorktree(id, worktreePath, mainWUPath) {
|
|
158
|
+
if (worktreePath) {
|
|
159
|
+
const wtWUPath = path.join(worktreePath, WU_PATHS.WU(id));
|
|
160
|
+
if (existsSync(wtWUPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const text = readFileSync(wtWUPath, { encoding: 'utf-8' });
|
|
163
|
+
const doc = parseYAML(text);
|
|
164
|
+
if (doc && doc.id === id) {
|
|
165
|
+
// WU-1584: Log source file for validation debugging
|
|
166
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Reading WU YAML from worktree: ${wtWUPath}`);
|
|
167
|
+
if (doc.code_paths && doc.code_paths.length > 0) {
|
|
168
|
+
console.log(`${LOG_PREFIX.DONE} code_paths source: worktree (${doc.code_paths.length} path(s))`);
|
|
169
|
+
}
|
|
170
|
+
return doc;
|
|
171
|
+
}
|
|
172
|
+
// If ID mismatch, log warning but continue
|
|
173
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Worktree YAML ID mismatch (expected ${id}, got ${doc?.id})`);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
// Log parse errors for debugging
|
|
177
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Failed to read worktree YAML: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// Log missing worktree YAML for debugging
|
|
182
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Worktree YAML not found at ${wtWUPath}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// WU-1584: Log when falling back to main checkout YAML
|
|
186
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Reading WU YAML from main: ${mainWUPath}`);
|
|
187
|
+
const doc = readWU(mainWUPath, id);
|
|
188
|
+
if (doc.code_paths && doc.code_paths.length > 0) {
|
|
189
|
+
console.log(`${LOG_PREFIX.DONE} code_paths source: main checkout (${doc.code_paths.length} path(s))`);
|
|
190
|
+
}
|
|
191
|
+
return doc;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Detect if currently running inside a worktree
|
|
195
|
+
* Checks for .git file (not directory) which indicates a worktree
|
|
196
|
+
* @returns {string|null} Current directory path if inside worktree, null otherwise
|
|
197
|
+
*/
|
|
198
|
+
export function detectCurrentWorktree() {
|
|
199
|
+
const cwd = process.cwd();
|
|
200
|
+
const gitPath = path.join(cwd, '.git');
|
|
201
|
+
// Check if .git exists and is a file (worktrees have .git file, main has .git directory)
|
|
202
|
+
if (!existsSync(gitPath))
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
const stats = statSync(gitPath);
|
|
206
|
+
if (stats.isFile()) {
|
|
207
|
+
// Parse .git file to verify it points to main repo's worktrees
|
|
208
|
+
const gitContent = readFileSync(gitPath, { encoding: 'utf-8' });
|
|
209
|
+
const match = gitContent.match(/^gitdir:\s*(.+)$/m);
|
|
210
|
+
if (match && match[1].includes('.git/worktrees/')) {
|
|
211
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.TARGET} Auto-detected worktree from process.cwd(): ${cwd}`);
|
|
212
|
+
return cwd;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
// Ignore errors, fall back to calculated path
|
|
218
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Failed to detect worktree: ${err.message}`);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Resolve worktree path from WU YAML
|
|
224
|
+
* Originally implemented in WU-1226, extracted to validators module in WU-1215
|
|
225
|
+
* Priority:
|
|
226
|
+
* 1. Read worktree_path field (set at claim time, immune to lane field changes)
|
|
227
|
+
* 2. Fall back to calculating from lane field (for old WUs without worktree_path)
|
|
228
|
+
* 3. Use git worktree list to find actual path (defensive fallback)
|
|
229
|
+
* @param {object} doc - WU YAML document
|
|
230
|
+
* @returns {Promise<string|null>} - Worktree path or null if not found
|
|
231
|
+
*/
|
|
232
|
+
export async function defaultWorktreeFrom(doc) {
|
|
233
|
+
// Priority 1 - use recorded worktree_path if available
|
|
234
|
+
if (doc.worktree_path) {
|
|
235
|
+
return doc.worktree_path;
|
|
236
|
+
}
|
|
237
|
+
// Priority 2 - calculate from current lane field (legacy behavior)
|
|
238
|
+
const lane = (doc.lane || '').toString();
|
|
239
|
+
const laneK = toKebab(lane);
|
|
240
|
+
const idK = (doc.id || '').toLowerCase();
|
|
241
|
+
if (!laneK || !idK)
|
|
242
|
+
return null;
|
|
243
|
+
const calculated = `worktrees/${laneK}-${idK}`;
|
|
244
|
+
// Priority 3 - verify calculated path exists, or find actual path via git worktree list
|
|
245
|
+
let calculatedExists = true;
|
|
246
|
+
try {
|
|
247
|
+
await access(calculated);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
calculatedExists = false;
|
|
251
|
+
}
|
|
252
|
+
if (!calculatedExists) {
|
|
253
|
+
try {
|
|
254
|
+
const worktreeList = await getGitForCwd().worktreeList();
|
|
255
|
+
const lines = worktreeList.split(STRING_LITERALS.NEWLINE);
|
|
256
|
+
const branch = `lane/${laneK}/${idK}`;
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
if (lines[i].startsWith('branch ') && lines[i].includes(branch)) {
|
|
259
|
+
// Found the branch, now get the worktree path from previous line
|
|
260
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
261
|
+
if (lines[j].startsWith('worktree ')) {
|
|
262
|
+
const fullPath = lines[j].substring('worktree '.length);
|
|
263
|
+
// Convert absolute path to relative path from repo root
|
|
264
|
+
const repoRoot = process.cwd();
|
|
265
|
+
const relativePath = path.relative(repoRoot, fullPath);
|
|
266
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree path mismatch detected:\n` +
|
|
267
|
+
` Expected: ${calculated}\n` +
|
|
268
|
+
` Actual: ${relativePath}\n` +
|
|
269
|
+
` Using actual path from git worktree list`);
|
|
270
|
+
return relativePath;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
console.warn(`${LOG_PREFIX.DONE} Could not query git worktree list: ${e.message}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return calculated;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Detect workspace mode from WU YAML
|
|
284
|
+
* @param {object} doc - WU YAML document
|
|
285
|
+
* @returns {'worktree' | 'branch-only'}
|
|
286
|
+
*/
|
|
287
|
+
export function detectWorkspaceMode(doc) {
|
|
288
|
+
// Explicit mode field takes precedence
|
|
289
|
+
if (doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY)
|
|
290
|
+
return CLAIMED_MODES.BRANCH_ONLY;
|
|
291
|
+
if (doc.claimed_mode === CLAIMED_MODES.WORKTREE)
|
|
292
|
+
return CLAIMED_MODES.WORKTREE;
|
|
293
|
+
// Backward compatibility: if claimed_mode is missing, assume worktree mode
|
|
294
|
+
// (all WUs claimed before WU-510 used worktree mode)
|
|
295
|
+
return CLAIMED_MODES.WORKTREE;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Calculate lane branch name from WU YAML
|
|
299
|
+
* @param {object} doc - WU YAML document
|
|
300
|
+
* @returns {string|null} Lane branch name (e.g., lane/operations-tooling/wu-1215)
|
|
301
|
+
*/
|
|
302
|
+
export function defaultBranchFrom(doc) {
|
|
303
|
+
const lane = (doc.lane || '').toString();
|
|
304
|
+
const laneK = toKebab(lane);
|
|
305
|
+
const idK = (doc.id || '').toLowerCase();
|
|
306
|
+
if (!laneK || !idK)
|
|
307
|
+
return null;
|
|
308
|
+
return `lane/${laneK}/${idK}`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Check if a branch exists
|
|
312
|
+
* @param {string} branch - Branch name to check
|
|
313
|
+
* @returns {Promise<boolean>} True if branch exists
|
|
314
|
+
*/
|
|
315
|
+
export async function branchExists(branch) {
|
|
316
|
+
return await getGitForCwd().branchExists(branch);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Detect workspace mode and calculate all relevant paths
|
|
320
|
+
* @param {string} id - WU ID
|
|
321
|
+
* @param {object} args - Parsed command-line arguments
|
|
322
|
+
* @returns {Promise<object>} Object containing paths, mode info, and WU document
|
|
323
|
+
*/
|
|
324
|
+
export async function detectModeAndPaths(id, args) {
|
|
325
|
+
const WU_PATH = WU_PATHS.WU(id);
|
|
326
|
+
const STATUS_PATH = WU_PATHS.STATUS();
|
|
327
|
+
const BACKLOG_PATH = WU_PATHS.BACKLOG();
|
|
328
|
+
const STAMPS_DIR = WU_PATHS.STAMPS_DIR();
|
|
329
|
+
// Read WU YAML to detect workspace mode
|
|
330
|
+
let docMain = readWU(WU_PATH, id);
|
|
331
|
+
const workspaceMode = detectWorkspaceMode(docMain);
|
|
332
|
+
const isBranchOnly = workspaceMode === CLAIMED_MODES.BRANCH_ONLY;
|
|
333
|
+
console.log(`\n${LOG_PREFIX.DONE} Detected workspace mode: ${workspaceMode}`);
|
|
334
|
+
// Determine candidate worktree path early (only relevant for Worktree mode)
|
|
335
|
+
// Priority: 1) Auto-detect from cwd 2) Explicit --worktree arg 3) Calculate from YAML
|
|
336
|
+
const detectedWorktree = detectCurrentWorktree();
|
|
337
|
+
const worktreePathGuess = args.worktree || null;
|
|
338
|
+
// For Worktree mode: prefer auto-detected worktree, then explicit arg, then calculated path
|
|
339
|
+
// For Branch-Only mode: use main checkout version (no worktree exists)
|
|
340
|
+
const derivedWorktree = isBranchOnly
|
|
341
|
+
? null
|
|
342
|
+
: detectedWorktree || worktreePathGuess || (await defaultWorktreeFrom(docMain));
|
|
343
|
+
if (!isBranchOnly && derivedWorktree && !detectedWorktree) {
|
|
344
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.FOLDER} Calculated worktree path from YAML: ${derivedWorktree}`);
|
|
345
|
+
}
|
|
346
|
+
// Read the actual WU YAML for validation (prefer worktree version over main)
|
|
347
|
+
const docForValidation = isBranchOnly
|
|
348
|
+
? docMain
|
|
349
|
+
: readWUPreferWorktree(id, derivedWorktree, WU_PATH);
|
|
350
|
+
// WU-1234: Detect docs-only by type OR by code_paths
|
|
351
|
+
// Auto-detect if all code_paths are under docs/, ai/, .claude/, or are README/CLAUDE files
|
|
352
|
+
const isDocsOnlyByType = docForValidation.type === 'documentation';
|
|
353
|
+
const isDocsOnlyByPaths = detectDocsOnlyByPaths(docForValidation.code_paths);
|
|
354
|
+
const isDocsOnly = isDocsOnlyByType || isDocsOnlyByPaths;
|
|
355
|
+
if (isDocsOnlyByPaths && !isDocsOnlyByType) {
|
|
356
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-detected docs-only WU from code_paths (type: ${docForValidation.type || 'unset'})`);
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
WU_PATH,
|
|
360
|
+
STATUS_PATH,
|
|
361
|
+
BACKLOG_PATH,
|
|
362
|
+
STAMPS_DIR,
|
|
363
|
+
docMain,
|
|
364
|
+
workspaceMode,
|
|
365
|
+
isBranchOnly,
|
|
366
|
+
derivedWorktree,
|
|
367
|
+
docForValidation,
|
|
368
|
+
isDocsOnly,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Generate commit message for WU completion
|
|
373
|
+
* Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
|
|
374
|
+
* @param {string} id - WU ID (e.g., "WU-1215")
|
|
375
|
+
* @param {string} title - WU title
|
|
376
|
+
* @param {number} maxLength - Maximum commit header length from commitlint config
|
|
377
|
+
* @returns {string} Formatted commit message
|
|
378
|
+
* @throws {Error} If generated message exceeds maxLength
|
|
379
|
+
*/
|
|
380
|
+
export function generateCommitMessage(id, title, maxLength = DEFAULTS.MAX_COMMIT_SUBJECT) {
|
|
381
|
+
const prefix = `wu(${id.toLowerCase()}): done - `;
|
|
382
|
+
const safe = String(title).trim().toLowerCase().replace(/\s+/g, ' ');
|
|
383
|
+
const room = Math.max(0, maxLength - prefix.length);
|
|
384
|
+
const short = safe.length > room ? `${safe.slice(0, room - 1)}…` : safe;
|
|
385
|
+
const msg = `${prefix}${short}`;
|
|
386
|
+
if (msg.length > maxLength) {
|
|
387
|
+
const error = new Error(`Commit message too long (${msg.length}/${maxLength}).\n` +
|
|
388
|
+
`Fix: Shorten WU title\n` +
|
|
389
|
+
`Current title: "${title}" (${title.length} chars)\n` +
|
|
390
|
+
`Suggested max: ~${maxLength - prefix.length} chars`);
|
|
391
|
+
error.code = 'COMMIT_MESSAGE_TOO_LONG';
|
|
392
|
+
error.data = {
|
|
393
|
+
title,
|
|
394
|
+
titleLength: title.length,
|
|
395
|
+
messageLength: msg.length,
|
|
396
|
+
maxLength,
|
|
397
|
+
suggestedMax: maxLength - prefix.length,
|
|
398
|
+
};
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
return msg;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Validate that required metadata files exist before updating
|
|
405
|
+
* WU-1275: Fail fast before mutations to prevent partial state
|
|
406
|
+
*
|
|
407
|
+
* @param {object} params - Parameters object
|
|
408
|
+
* @param {string} params.statusPath - Path to status.md file
|
|
409
|
+
* @param {string} params.backlogPath - Path to backlog.md file
|
|
410
|
+
* @throws {WUError} If any required file is missing
|
|
411
|
+
*/
|
|
412
|
+
export function validateMetadataFilesExist({ statusPath, backlogPath }) {
|
|
413
|
+
const missing = [];
|
|
414
|
+
if (!existsSync(statusPath)) {
|
|
415
|
+
missing.push(`Status: ${statusPath}`);
|
|
416
|
+
}
|
|
417
|
+
if (!existsSync(backlogPath)) {
|
|
418
|
+
missing.push(`Backlog: ${backlogPath}`);
|
|
419
|
+
}
|
|
420
|
+
if (missing.length > 0) {
|
|
421
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Required metadata files missing:\n ${missing.join('\n ')}\n\nCannot complete WU - verify worktree has latest metadata files.`, { missingFiles: missing });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Update all metadata files for WU completion
|
|
426
|
+
* Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
|
|
427
|
+
* WU-1572: Made async for WUStateStore integration
|
|
428
|
+
* @param {object} params - Parameters object
|
|
429
|
+
* @param {string} params.id - WU ID
|
|
430
|
+
* @param {string} params.title - WU title
|
|
431
|
+
* @param {object} params.doc - WU YAML document to update
|
|
432
|
+
* @param {string} params.wuPath - Path to WU YAML file
|
|
433
|
+
* @param {string} params.statusPath - Path to status.md file
|
|
434
|
+
* @param {string} params.backlogPath - Path to backlog.md file
|
|
435
|
+
*/
|
|
436
|
+
export async function updateMetadataFiles({ id, title, doc, wuPath, statusPath, backlogPath }) {
|
|
437
|
+
// WU-1275: Fail fast before any mutations
|
|
438
|
+
validateMetadataFilesExist({ statusPath, backlogPath });
|
|
439
|
+
// Update WU YAML (mark as done, lock, set completion timestamp)
|
|
440
|
+
doc.status = 'done';
|
|
441
|
+
doc.locked = true;
|
|
442
|
+
doc.completed_at = new Date().toISOString();
|
|
443
|
+
writeWU(wuPath, doc);
|
|
444
|
+
// Update status.md (remove from In Progress, add to Completed)
|
|
445
|
+
updateStatusRemoveInProgress(statusPath, id);
|
|
446
|
+
addToStatusCompleted(statusPath, id, title);
|
|
447
|
+
// Update backlog.md (move to Done section)
|
|
448
|
+
// WU-1572: Now async for state store integration
|
|
449
|
+
await moveWUToDoneBacklog(backlogPath, id, title);
|
|
450
|
+
// Create completion stamp
|
|
451
|
+
createStamp({ id, title });
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Collect metadata updates to a transaction (WU-1369: Atomic pattern)
|
|
455
|
+
*
|
|
456
|
+
* This is the atomic version of updateMetadataFiles.
|
|
457
|
+
* Instead of writing files immediately, it collects all changes
|
|
458
|
+
* into a WUTransaction object for atomic commit.
|
|
459
|
+
*
|
|
460
|
+
* Usage:
|
|
461
|
+
* ```js
|
|
462
|
+
* const tx = new WUTransaction(id);
|
|
463
|
+
* collectMetadataToTransaction({ id, title, doc, wuPath, statusPath, backlogPath, stampPath, transaction: tx });
|
|
464
|
+
* // All changes are now in tx.pendingWrites
|
|
465
|
+
* // Validate, then commit or abort
|
|
466
|
+
* tx.commit();
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @param {object} params - Parameters object
|
|
470
|
+
* @param {string} params.id - WU ID
|
|
471
|
+
* @param {string} params.title - WU title
|
|
472
|
+
* @param {object} params.doc - WU YAML document to update (will be mutated)
|
|
473
|
+
* @param {string} params.wuPath - Path to WU YAML file
|
|
474
|
+
* @param {string} params.statusPath - Path to status.md file
|
|
475
|
+
* @param {string} params.backlogPath - Path to backlog.md file
|
|
476
|
+
* @param {string} params.stampPath - Path to stamp file
|
|
477
|
+
* @param {WUTransaction} params.transaction - Transaction to add writes to
|
|
478
|
+
*/
|
|
479
|
+
// WU-1574: Made async for computeBacklogContent
|
|
480
|
+
export async function collectMetadataToTransaction({ id, title, doc, wuPath, statusPath, backlogPath, stampPath, transaction, }) {
|
|
481
|
+
// WU-1369: Fail fast before any computations
|
|
482
|
+
validateMetadataFilesExist({ statusPath, backlogPath });
|
|
483
|
+
// Compute WU YAML content (mutates doc, returns YAML string)
|
|
484
|
+
const wuYAMLContent = computeWUYAMLContent(doc);
|
|
485
|
+
transaction.addWrite(wuPath, wuYAMLContent, 'WU YAML');
|
|
486
|
+
// Compute status.md content
|
|
487
|
+
const statusContent = computeStatusContent(statusPath, id, title);
|
|
488
|
+
transaction.addWrite(statusPath, statusContent, 'status.md');
|
|
489
|
+
// Compute backlog.md content (WU-1574: now async)
|
|
490
|
+
const backlogContent = await computeBacklogContent(backlogPath, id, title);
|
|
491
|
+
transaction.addWrite(backlogPath, backlogContent, 'backlog.md');
|
|
492
|
+
const wuEventsUpdate = await computeWUEventsContentAfterComplete(backlogPath, id);
|
|
493
|
+
if (wuEventsUpdate) {
|
|
494
|
+
transaction.addWrite(wuEventsUpdate.eventsPath, wuEventsUpdate.content, 'wu-events.jsonl');
|
|
495
|
+
}
|
|
496
|
+
// Compute stamp content
|
|
497
|
+
const stampContent = computeStampContent(id, title);
|
|
498
|
+
transaction.addWrite(stampPath, stampContent, 'completion stamp');
|
|
499
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Collected ${transaction.size} metadata updates for atomic commit`);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Stage and format metadata files
|
|
503
|
+
* Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
|
|
504
|
+
* @param {object} params - Parameters object
|
|
505
|
+
* @param {string} params.id - WU ID (for error reporting)
|
|
506
|
+
* @param {string} params.wuPath - Path to WU YAML file
|
|
507
|
+
* @param {string} params.statusPath - Path to status.md file
|
|
508
|
+
* @param {string} params.backlogPath - Path to backlog.md file
|
|
509
|
+
* @param {string} params.stampsDir - Path to stamps directory
|
|
510
|
+
* @throws {Error} If formatting fails
|
|
511
|
+
*/
|
|
512
|
+
export async function stageAndFormatMetadata({ id, wuPath, statusPath, backlogPath, stampsDir }) {
|
|
513
|
+
// WU-1235: Use getGitForCwd() to capture current directory (worktree after chdir)
|
|
514
|
+
// The singleton git adapter captures cwd at import time, which is wrong after process.chdir()
|
|
515
|
+
const gitCwd = getGitForCwd();
|
|
516
|
+
// Stage files
|
|
517
|
+
const wuEventsPath = path.join(BEACON_PATHS.STATE_DIR, WU_EVENTS_FILE_NAME);
|
|
518
|
+
const filesToStage = [wuPath, statusPath, backlogPath, stampsDir];
|
|
519
|
+
if (existsSync(wuEventsPath)) {
|
|
520
|
+
filesToStage.push(wuEventsPath);
|
|
521
|
+
}
|
|
522
|
+
await gitCwd.add(filesToStage);
|
|
523
|
+
// Format documentation
|
|
524
|
+
console.log(`${LOG_PREFIX.DONE} Formatting auto-generated documentation...`);
|
|
525
|
+
try {
|
|
526
|
+
const prettierCmd = `${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} "${wuPath}" "${statusPath}" "${backlogPath}"`;
|
|
527
|
+
await execAsync(prettierCmd);
|
|
528
|
+
await gitCwd.add([wuPath, statusPath, backlogPath]);
|
|
529
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Documentation formatted`);
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `Failed to format documentation: ${err.message}`, { wuId: id, error: err.message });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Run cleanup operations after successful merge
|
|
537
|
+
* Removes worktree and optionally deletes lane branch
|
|
538
|
+
* Extracted from wu-done.mjs (WU-1215 Phase 1 Extraction #3)
|
|
539
|
+
*
|
|
540
|
+
* WU-2241: Now wrapped with cleanup lock to prevent concurrent collision
|
|
541
|
+
* when multiple wu:done commands complete simultaneously.
|
|
542
|
+
*
|
|
543
|
+
* @param {object} docMain - WU YAML document
|
|
544
|
+
* @param {object} args - Parsed CLI arguments
|
|
545
|
+
*/
|
|
546
|
+
export async function runCleanup(docMain, args) {
|
|
547
|
+
const wuId = docMain.id;
|
|
548
|
+
const worktreePath = args.worktree || (await defaultWorktreeFrom(docMain));
|
|
549
|
+
// WU-2278: Validate worktree ownership before cleanup
|
|
550
|
+
// Prevents cross-agent worktree deletion
|
|
551
|
+
if (!args.overrideOwner) {
|
|
552
|
+
const ownershipResult = validateWorktreeOwnership({ worktreePath, wuId });
|
|
553
|
+
if (!ownershipResult.valid) {
|
|
554
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ownershipResult.error}\n\nTo override (DANGEROUS): pnpm wu:done --id ${wuId} --override-owner --reason "explanation"`, { wuId, worktreePath, error: ownershipResult.error });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// WU-2241: Wrap cleanup operations in cleanup lock to prevent concurrent collision
|
|
558
|
+
await withCleanupLock(wuId, async () => {
|
|
559
|
+
await runCleanupInternal(docMain, args, worktreePath);
|
|
560
|
+
}, { worktreePath });
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Internal cleanup implementation (runs under cleanup lock)
|
|
564
|
+
*
|
|
565
|
+
* @param {object} docMain - WU YAML document
|
|
566
|
+
* @param {object} args - Parsed CLI arguments
|
|
567
|
+
* @param {string|null} worktreePath - Path to worktree
|
|
568
|
+
*/
|
|
569
|
+
async function runCleanupInternal(docMain, args, worktreePath) {
|
|
570
|
+
// Step 6: Remove worktree (runs even if commit/push failed)
|
|
571
|
+
// Skip removal in PR mode (worktree needed for cleanup after PR merge)
|
|
572
|
+
const claimedMode = docMain.claimed_mode || CLAIMED_MODES.WORKTREE;
|
|
573
|
+
const requiresReview = docMain.requires_review === true;
|
|
574
|
+
const prModeEnabled = claimedMode === CLAIMED_MODES.WORKTREE_PR || args.createPR || requiresReview;
|
|
575
|
+
// WU-2241: Track branch for cleanup after worktree removal
|
|
576
|
+
const laneBranch = await defaultBranchFrom(docMain);
|
|
577
|
+
if (!args.noRemove && !prModeEnabled) {
|
|
578
|
+
if (worktreePath && existsSync(worktreePath)) {
|
|
579
|
+
try {
|
|
580
|
+
await getGitForCwd().worktreeRemove(worktreePath, { force: true });
|
|
581
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed worktree ${worktreePath}`);
|
|
582
|
+
// WU-2241: Delete branch AFTER worktree removal (correct ordering)
|
|
583
|
+
// This ensures we don't leave orphan branches when worktree is removed
|
|
584
|
+
if (laneBranch && (await branchExists(laneBranch))) {
|
|
585
|
+
await deleteBranchWithCleanup(laneBranch);
|
|
586
|
+
}
|
|
587
|
+
// WU-1743: Re-run pnpm install to fix broken symlinks
|
|
588
|
+
// When pnpm install runs in a worktree, it may create symlinks with absolute paths
|
|
589
|
+
// to the worktree. After worktree removal, these symlinks break.
|
|
590
|
+
// Re-running pnpm install regenerates them with correct paths.
|
|
591
|
+
// WU-2278: Use timeout and CI=true to prevent hangs
|
|
592
|
+
console.log(`${LOG_PREFIX.DONE} Reinstalling dependencies to fix symlinks...`);
|
|
593
|
+
try {
|
|
594
|
+
const installConfig = getCleanupInstallConfig();
|
|
595
|
+
await execAsync(installConfig.command, {
|
|
596
|
+
timeout: installConfig.timeout,
|
|
597
|
+
env: installConfig.env,
|
|
598
|
+
});
|
|
599
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Dependencies reinstalled`);
|
|
600
|
+
}
|
|
601
|
+
catch (installErr) {
|
|
602
|
+
// Non-fatal: warn but don't fail wu:done
|
|
603
|
+
// WU-2278: Include timeout info in error message
|
|
604
|
+
const isTimeout = installErr.killed || installErr.signal === 'SIGTERM';
|
|
605
|
+
const errorMsg = isTimeout
|
|
606
|
+
? `pnpm install timed out after ${CLEANUP_INSTALL_TIMEOUT_MS / 1000}s`
|
|
607
|
+
: `pnpm install failed: ${installErr.message}`;
|
|
608
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} ${errorMsg}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
console.warn(`${LOG_PREFIX.DONE} Could not remove worktree ${worktreePath}: ${e.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
console.log(`${LOG_PREFIX.DONE} Worktree not found; skipping removal`);
|
|
617
|
+
// WU-2241: Still cleanup branch if worktree doesn't exist (orphan branch scenario)
|
|
618
|
+
if (!prModeEnabled && laneBranch && (await branchExists(laneBranch))) {
|
|
619
|
+
await deleteBranchWithCleanup(laneBranch);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else if (prModeEnabled) {
|
|
624
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree preserved (PR mode - run wu:cleanup after PR merge)`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* WU-2241: Delete both local and remote branch with proper error handling
|
|
629
|
+
*
|
|
630
|
+
* @param {string} laneBranch - Branch name to delete
|
|
631
|
+
*/
|
|
632
|
+
async function deleteBranchWithCleanup(laneBranch) {
|
|
633
|
+
const gitAdapter = getGitForCwd();
|
|
634
|
+
// WU-1440: Check if branch is merged before deletion
|
|
635
|
+
// Use -D (force) when confirmed merged to handle rebased branches
|
|
636
|
+
const isMerged = await isBranchAlreadyMerged(laneBranch);
|
|
637
|
+
try {
|
|
638
|
+
await gitAdapter.deleteBranch(laneBranch, { force: isMerged });
|
|
639
|
+
const modeIndicator = isMerged ? ' (force: merged)' : '';
|
|
640
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Deleted local branch ${laneBranch}${modeIndicator}`);
|
|
641
|
+
// Also delete remote if it exists
|
|
642
|
+
try {
|
|
643
|
+
await gitAdapter.raw(['push', REMOTES.ORIGIN, '--delete', laneBranch]);
|
|
644
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Deleted remote branch ${laneBranch}`);
|
|
645
|
+
}
|
|
646
|
+
catch (e) {
|
|
647
|
+
// WU-2241: Non-fatal - remote branch may already be deleted or never existed
|
|
648
|
+
console.warn(`${LOG_PREFIX.DONE} Could not delete remote branch: ${e.message}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
console.warn(`${LOG_PREFIX.DONE} Could not delete branch ${laneBranch}: ${e.message}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
export async function validateCodePathsExist(doc, id, options = {}) {
|
|
656
|
+
const { targetBranch = BRANCHES.MAIN, worktreePath = null } = options;
|
|
657
|
+
const errors = [];
|
|
658
|
+
const missing = [];
|
|
659
|
+
const codePaths = doc.code_paths || [];
|
|
660
|
+
// Skip validation for WUs without code_paths (docs-only, process WUs)
|
|
661
|
+
if (codePaths.length === 0) {
|
|
662
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No code_paths to validate for ${id}`);
|
|
663
|
+
return { valid: true, errors: [], missing: [] };
|
|
664
|
+
}
|
|
665
|
+
console.log(`${LOG_PREFIX.DONE} Validating ${codePaths.length} code_paths exist...`);
|
|
666
|
+
// For worktree mode, check files exist in the worktree (will be merged)
|
|
667
|
+
// For branch-only mode or post-merge validation, check files exist on target branch
|
|
668
|
+
if (worktreePath && existsSync(worktreePath)) {
|
|
669
|
+
// Worktree mode: validate files exist in worktree
|
|
670
|
+
for (const filePath of codePaths) {
|
|
671
|
+
const fullPath = path.join(worktreePath, filePath);
|
|
672
|
+
if (!existsSync(fullPath)) {
|
|
673
|
+
missing.push(filePath);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (missing.length > 0) {
|
|
677
|
+
errors.push(`code_paths validation failed - ${missing.length} file(s) not found in worktree:\n${missing
|
|
678
|
+
.map((p) => ` - ${p}`)
|
|
679
|
+
.join(STRING_LITERALS.NEWLINE)}\n\nEnsure all files listed in code_paths exist before running wu:done.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// Branch-only or post-merge: use git ls-tree to check files on target branch
|
|
684
|
+
try {
|
|
685
|
+
const gitAdapter = getGitForCwd();
|
|
686
|
+
for (const filePath of codePaths) {
|
|
687
|
+
try {
|
|
688
|
+
// git ls-tree returns empty for non-existent files
|
|
689
|
+
const result = await gitAdapter.raw([GIT_COMMANDS.LS_TREE, targetBranch, '--', filePath]);
|
|
690
|
+
if (!result || result.trim() === '') {
|
|
691
|
+
missing.push(filePath);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
// git ls-tree fails for non-existent paths
|
|
696
|
+
missing.push(filePath);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (missing.length > 0) {
|
|
700
|
+
errors.push(`code_paths validation failed - ${missing.length} file(s) not found on ${targetBranch}:\n${missing
|
|
701
|
+
.map((p) => ` - ${p}`)
|
|
702
|
+
.join(STRING_LITERALS.NEWLINE)}\n\n❌ POTENTIAL FALSE COMPLETION DETECTED\n\n` +
|
|
703
|
+
`These files are listed in code_paths but do not exist on ${targetBranch}.\n` +
|
|
704
|
+
`This prevents creating a stamp for incomplete work.\n\n` +
|
|
705
|
+
`Fix options:\n` +
|
|
706
|
+
` 1. Ensure all code is committed and merged to ${targetBranch}\n` +
|
|
707
|
+
` 2. Update code_paths in ${id}.yaml to match actual files\n` +
|
|
708
|
+
` 3. Remove files that were intentionally not created\n\n` +
|
|
709
|
+
`Context: WU-1351 prevents false completions from INIT-WORKFLOW-INTEGRITY`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
// Non-fatal: warn but don't block if git command fails
|
|
714
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not validate code_paths: ${err.message}`);
|
|
715
|
+
return { valid: true, errors: [], missing: [] };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (errors.length > 0) {
|
|
719
|
+
return { valid: false, errors, missing };
|
|
720
|
+
}
|
|
721
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All ${codePaths.length} code_paths verified`);
|
|
722
|
+
return { valid: true, errors: [], missing: [] };
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Validate WU spec completeness (WU-1162, WU-1280)
|
|
726
|
+
*
|
|
727
|
+
* Ensures WU specifications are complete before allowing wu:done to proceed.
|
|
728
|
+
* Prevents placeholder WUs from being marked as done.
|
|
729
|
+
*
|
|
730
|
+
* WU-1280: Added tests array validation to catch empty tests.manual early
|
|
731
|
+
* (previously only validated in pre-commit hook, causing late failures).
|
|
732
|
+
*
|
|
733
|
+
* @param {object} doc - WU YAML document
|
|
734
|
+
* @param {string} id - WU ID
|
|
735
|
+
* @returns {{ valid: boolean, errors: string[] }} Validation result
|
|
736
|
+
*/
|
|
737
|
+
export function validateSpecCompleteness(doc, _id) {
|
|
738
|
+
const errors = [];
|
|
739
|
+
// Check for placeholder text in description
|
|
740
|
+
if (doc.description && doc.description.includes(PLACEHOLDER_SENTINEL)) {
|
|
741
|
+
errors.push(`Description contains ${PLACEHOLDER_SENTINEL} marker`);
|
|
742
|
+
}
|
|
743
|
+
// Handle both array and object formats for acceptance criteria
|
|
744
|
+
if (doc.acceptance) {
|
|
745
|
+
const hasPlaceholder = (value) => {
|
|
746
|
+
if (typeof value === 'string') {
|
|
747
|
+
return value.includes(PLACEHOLDER_SENTINEL);
|
|
748
|
+
}
|
|
749
|
+
if (Array.isArray(value)) {
|
|
750
|
+
return value.some((item) => hasPlaceholder(item));
|
|
751
|
+
}
|
|
752
|
+
if (typeof value === 'object' && value !== null) {
|
|
753
|
+
return Object.values(value).some((item) => hasPlaceholder(item));
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
};
|
|
757
|
+
if (hasPlaceholder(doc.acceptance)) {
|
|
758
|
+
errors.push(`Acceptance criteria contain ${PLACEHOLDER_SENTINEL} markers`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Check minimum description length
|
|
762
|
+
// WU-1281: Using centralized constant from wu-constants.mjs
|
|
763
|
+
if (!doc.description || doc.description.trim().length < VALIDATION.MIN_DESCRIPTION_LENGTH) {
|
|
764
|
+
errors.push(`Description too short (${doc.description?.trim().length || 0} chars, minimum ${VALIDATION.MIN_DESCRIPTION_LENGTH})`);
|
|
765
|
+
}
|
|
766
|
+
// Check code_paths for non-documentation WUs
|
|
767
|
+
// WU-1281: Using centralized type constants from wu-constants.mjs
|
|
768
|
+
if (doc.type !== WU_TYPES.DOCUMENTATION && doc.type !== WU_TYPES.PROCESS) {
|
|
769
|
+
if (!doc.code_paths || doc.code_paths.length === 0) {
|
|
770
|
+
errors.push('Code paths required for non-documentation WUs');
|
|
771
|
+
}
|
|
772
|
+
// WU-1280: Check tests array for non-documentation WUs
|
|
773
|
+
// Support both tests: (current) and test_paths: (legacy)
|
|
774
|
+
const testObj = doc.tests || doc.test_paths || {};
|
|
775
|
+
// Helper to check if array has items
|
|
776
|
+
const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
|
|
777
|
+
// WU-1281: Using centralized test type constants from wu-constants.mjs
|
|
778
|
+
const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
|
|
779
|
+
const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
|
|
780
|
+
const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
|
|
781
|
+
const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
|
|
782
|
+
if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
|
|
783
|
+
errors.push('At least one test path required (unit, e2e, integration, or manual)');
|
|
784
|
+
}
|
|
785
|
+
// WU-2332: Require automated tests for code file changes
|
|
786
|
+
// Manual-only tests are not sufficient when code_paths contain actual code files
|
|
787
|
+
const automatedTestResult = validateAutomatedTestRequirement(doc);
|
|
788
|
+
if (!automatedTestResult.valid) {
|
|
789
|
+
errors.push(...automatedTestResult.errors);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return { valid: errors.length === 0, errors };
|
|
793
|
+
}
|
|
794
|
+
// WU-1433: Re-export manual test validator for use in wu:done workflow
|
|
795
|
+
export { validateAutomatedTestRequirement };
|
|
796
|
+
/**
|
|
797
|
+
* WU-1617: Post-mutation validation for wu:done
|
|
798
|
+
*
|
|
799
|
+
* Validates that metadata files written by tx.commit() are valid:
|
|
800
|
+
* 1. WU YAML has completed_at field with valid ISO datetime
|
|
801
|
+
* 2. WU YAML has locked: true
|
|
802
|
+
* 3. Stamp file exists
|
|
803
|
+
*
|
|
804
|
+
* This catches schema violations that could persist silently after
|
|
805
|
+
* transaction commit.
|
|
806
|
+
*
|
|
807
|
+
* @param {object} params - Validation parameters
|
|
808
|
+
* @param {string} params.id - WU ID
|
|
809
|
+
* @param {string} params.wuPath - Path to WU YAML file
|
|
810
|
+
* @param {string} params.stampPath - Path to stamp file
|
|
811
|
+
* @returns {{ valid: boolean, errors: string[] }} Validation result
|
|
812
|
+
*/
|
|
813
|
+
export function validatePostMutation({ id, wuPath, stampPath }) {
|
|
814
|
+
const errors = [];
|
|
815
|
+
// Check stamp file exists
|
|
816
|
+
if (!existsSync(stampPath)) {
|
|
817
|
+
errors.push(`Stamp file not created: ${stampPath}`);
|
|
818
|
+
}
|
|
819
|
+
// Read and validate WU YAML after mutation
|
|
820
|
+
if (!existsSync(wuPath)) {
|
|
821
|
+
errors.push(`WU YAML not found after mutation: ${wuPath}`);
|
|
822
|
+
return { valid: false, errors };
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
const content = readFileSync(wuPath, { encoding: 'utf-8' });
|
|
826
|
+
const doc = parseYAML(content);
|
|
827
|
+
// Verify completed_at exists and is valid ISO datetime
|
|
828
|
+
if (!doc.completed_at) {
|
|
829
|
+
errors.push(`Missing required field 'completed_at' in ${id}.yaml`);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// Validate ISO datetime format (YYYY-MM-DDTHH:mm:ss.sssZ or similar)
|
|
833
|
+
const timestamp = new Date(doc.completed_at);
|
|
834
|
+
if (isNaN(timestamp.getTime())) {
|
|
835
|
+
errors.push(`Invalid completed_at timestamp: ${doc.completed_at}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Verify locked is true
|
|
839
|
+
if (doc.locked !== true) {
|
|
840
|
+
errors.push(`Missing or invalid 'locked' field in ${id}.yaml (expected: true, got: ${doc.locked})`);
|
|
841
|
+
}
|
|
842
|
+
// Verify status is done
|
|
843
|
+
if (doc.status !== 'done') {
|
|
844
|
+
errors.push(`Invalid status in ${id}.yaml (expected: 'done', got: '${doc.status}')`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch (err) {
|
|
848
|
+
errors.push(`Failed to parse WU YAML after mutation: ${err.message}`);
|
|
849
|
+
}
|
|
850
|
+
return { valid: errors.length === 0, errors };
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* WU-1781: Build preflight error message with actionable guidance
|
|
854
|
+
*
|
|
855
|
+
* Creates a formatted error message for preflight validation failures,
|
|
856
|
+
* including specific guidance for stamp-status mismatch errors.
|
|
857
|
+
*
|
|
858
|
+
* @param {string} id - WU ID being completed
|
|
859
|
+
* @param {string[]} errors - List of validation errors
|
|
860
|
+
* @returns {string} Formatted error message with fix options
|
|
861
|
+
*/
|
|
862
|
+
export function buildPreflightErrorMessage(id, errors) {
|
|
863
|
+
const hasStampStatusError = errors.some((e) => e.includes('stamp but status is not done'));
|
|
864
|
+
let message = `
|
|
865
|
+
❌ PREFLIGHT VALIDATION FAILED
|
|
866
|
+
|
|
867
|
+
tasks:validate found errors that would block pre-push hooks.
|
|
868
|
+
Aborting wu:done BEFORE any merge operations to prevent deadlocks.
|
|
869
|
+
|
|
870
|
+
Errors:
|
|
871
|
+
${errors.map((e) => ` - ${e}`).join('\n')}
|
|
872
|
+
|
|
873
|
+
Fix options:
|
|
874
|
+
`;
|
|
875
|
+
if (hasStampStatusError) {
|
|
876
|
+
message += `
|
|
877
|
+
For stamp-status mismatch errors:
|
|
878
|
+
1. Fix the WU status to match the stamp (set status: done, locked: true)
|
|
879
|
+
2. Or add the WU ID to .lumenflow.config.yaml > exemptions > stamp_status_mismatch
|
|
880
|
+
|
|
881
|
+
`;
|
|
882
|
+
}
|
|
883
|
+
message += `
|
|
884
|
+
General fixes:
|
|
885
|
+
1. Run: pnpm tasks:validate to see full errors
|
|
886
|
+
2. Fix the validation errors
|
|
887
|
+
3. Retry: pnpm wu:done --id ${id}
|
|
888
|
+
|
|
889
|
+
This preflight check prevents wu:done from leaving main in a stuck state
|
|
890
|
+
where husky pre-push would block all further operations.
|
|
891
|
+
`;
|
|
892
|
+
return message;
|
|
893
|
+
}
|
|
894
|
+
export async function executePreflightCodePathValidation(id, paths, options = {}) {
|
|
895
|
+
// Use injected validator for testability, default to actual implementation
|
|
896
|
+
const validatePreflightFn = options.validatePreflightFn || validatePreflight;
|
|
897
|
+
console.log(`\n${LOG_PREFIX.DONE} 🔍 Preflight: validating code_paths and test paths...`);
|
|
898
|
+
const result = await validatePreflightFn(id, {
|
|
899
|
+
rootDir: paths.rootDir,
|
|
900
|
+
worktreePath: paths.worktreePath,
|
|
901
|
+
});
|
|
902
|
+
if (result.valid) {
|
|
903
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Preflight code_paths validation passed`);
|
|
904
|
+
return {
|
|
905
|
+
valid: true,
|
|
906
|
+
errors: [],
|
|
907
|
+
missingCodePaths: [],
|
|
908
|
+
missingTestPaths: [],
|
|
909
|
+
abortedBeforeGates: false,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Preflight code_paths validation failed`);
|
|
913
|
+
return {
|
|
914
|
+
valid: false,
|
|
915
|
+
errors: result.errors,
|
|
916
|
+
missingCodePaths: result.missingCodePaths || [],
|
|
917
|
+
missingTestPaths: result.missingTestPaths || [],
|
|
918
|
+
abortedBeforeGates: true,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* WU-1805: Build preflight code_paths error message with actionable guidance
|
|
923
|
+
*
|
|
924
|
+
* Creates a formatted error message for preflight code_paths validation failures,
|
|
925
|
+
* including specific guidance for fixing missing files.
|
|
926
|
+
*
|
|
927
|
+
* @param {string} id - WU ID being completed
|
|
928
|
+
* @param {object} result - Preflight validation result
|
|
929
|
+
* @param {string[]} result.errors - List of validation errors
|
|
930
|
+
* @param {string[]} result.missingCodePaths - Missing code_paths files
|
|
931
|
+
* @param {string[]} result.missingTestPaths - Missing test files
|
|
932
|
+
* @returns {string} Formatted error message with fix options
|
|
933
|
+
*/
|
|
934
|
+
export function buildPreflightCodePathErrorMessage(id, result) {
|
|
935
|
+
const { errors, missingCodePaths = [], missingTestPaths = [] } = result;
|
|
936
|
+
let message = `
|
|
937
|
+
❌ PREFLIGHT CODE_PATHS VALIDATION FAILED
|
|
938
|
+
|
|
939
|
+
code_paths/test_paths validation found errors that would cause gates to fail.
|
|
940
|
+
Aborting wu:done BEFORE running gates to save time.
|
|
941
|
+
|
|
942
|
+
Errors:
|
|
943
|
+
${errors.map((e) => ` ${e}`).join('\n')}
|
|
944
|
+
|
|
945
|
+
`;
|
|
946
|
+
if (missingCodePaths.length > 0) {
|
|
947
|
+
message += `
|
|
948
|
+
Fix options for missing code_paths:
|
|
949
|
+
1. Create the missing files in your worktree
|
|
950
|
+
2. Update code_paths in ${id}.yaml using: pnpm wu:edit --id ${id} --code-paths "<corrected-paths>"
|
|
951
|
+
3. Remove paths that were intentionally not created
|
|
952
|
+
|
|
953
|
+
`;
|
|
954
|
+
}
|
|
955
|
+
if (missingTestPaths.length > 0) {
|
|
956
|
+
message += `
|
|
957
|
+
Fix options for missing test_paths:
|
|
958
|
+
1. Create the missing test files
|
|
959
|
+
2. Update test paths in ${id}.yaml using wu:edit
|
|
960
|
+
3. Use tests.manual for descriptions instead of file paths
|
|
961
|
+
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
message += `
|
|
965
|
+
After fixing, retry:
|
|
966
|
+
pnpm wu:done --id ${id}
|
|
967
|
+
|
|
968
|
+
This preflight check runs BEFORE gates to catch YAML mismatches early.
|
|
969
|
+
See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.
|
|
970
|
+
`;
|
|
971
|
+
return message;
|
|
972
|
+
}
|
|
973
|
+
export function runPreflightTasksValidation(id, options = {}) {
|
|
974
|
+
// Use injected execSync for testability, default to node's child_process
|
|
975
|
+
const execSyncFn = options.execSyncFn || execSyncImport;
|
|
976
|
+
console.log(`\n${LOG_PREFIX.DONE} 🔍 Preflight: running tasks:validate...`);
|
|
977
|
+
try {
|
|
978
|
+
// Run tasks:validate with WU_ID context (single-WU validation mode)
|
|
979
|
+
execSyncFn('node tools/validate.js', {
|
|
980
|
+
stdio: STDIO.PIPE,
|
|
981
|
+
encoding: 'utf-8',
|
|
982
|
+
env: { ...process.env, WU_ID: id },
|
|
983
|
+
});
|
|
984
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Preflight tasks:validate passed`);
|
|
985
|
+
return {
|
|
986
|
+
valid: true,
|
|
987
|
+
errors: [],
|
|
988
|
+
abortedBeforeMerge: false,
|
|
989
|
+
localMainModified: false,
|
|
990
|
+
hasStampStatusError: false,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
// Validation failed - extract errors from output
|
|
995
|
+
const output = err.stdout || err.message || 'Unknown validation error';
|
|
996
|
+
const errors = output
|
|
997
|
+
.split('\n')
|
|
998
|
+
.filter((line) => line.includes('[') && line.includes(']'))
|
|
999
|
+
.map((line) => line.trim());
|
|
1000
|
+
const hasStampStatusError = errors.some((e) => e.includes('stamp but status is not done'));
|
|
1001
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Preflight tasks:validate failed`);
|
|
1002
|
+
return {
|
|
1003
|
+
valid: false,
|
|
1004
|
+
errors: errors.length > 0 ? errors : [output],
|
|
1005
|
+
abortedBeforeMerge: true,
|
|
1006
|
+
localMainModified: false,
|
|
1007
|
+
hasStampStatusError,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* WU-2308: Validate all pre-commit hooks with worktree context
|
|
1013
|
+
*
|
|
1014
|
+
* Runs pre-commit validation gates from the worktree directory when provided.
|
|
1015
|
+
* This ensures that dependency audits check the worktree's dependencies
|
|
1016
|
+
* (with any fixes) rather than main's potentially stale dependencies.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {string} id - WU ID being completed
|
|
1019
|
+
* @param {string|null} worktreePath - Path to worktree (null = run from current dir)
|
|
1020
|
+
* @param {ExecSyncOverrideOptions} options - Options for testing
|
|
1021
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
1022
|
+
*/
|
|
1023
|
+
export function validateAllPreCommitHooks(id, worktreePath = null, options = {}) {
|
|
1024
|
+
const execSyncFn = options.execSyncFn || execSyncImport;
|
|
1025
|
+
console.log(`\n${LOG_PREFIX.DONE} 🔍 Pre-flight: validating all pre-commit hooks...`);
|
|
1026
|
+
const errors = [];
|
|
1027
|
+
try {
|
|
1028
|
+
// WU-2308: Run from worktree context when provided to ensure audit checks
|
|
1029
|
+
// the worktree's dependencies (with fixes) not main's stale dependencies
|
|
1030
|
+
const execOptions = {
|
|
1031
|
+
stdio: STDIO.INHERIT,
|
|
1032
|
+
encoding: 'utf-8',
|
|
1033
|
+
};
|
|
1034
|
+
// Only set cwd when worktreePath is provided
|
|
1035
|
+
if (worktreePath) {
|
|
1036
|
+
execOptions.cwd = worktreePath;
|
|
1037
|
+
}
|
|
1038
|
+
// Run the gates-pre-commit script that contains all validation gates
|
|
1039
|
+
execSyncFn('node tools/gates-pre-commit.js', execOptions);
|
|
1040
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All pre-commit hooks passed`);
|
|
1041
|
+
return { valid: true, errors: [] };
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// Pre-commit hooks failed
|
|
1045
|
+
errors.push('Pre-commit hook validation failed. Fix these issues before wu:done:');
|
|
1046
|
+
errors.push('');
|
|
1047
|
+
errors.push('Common fixes:');
|
|
1048
|
+
errors.push(' • Formatting issues: Run pnpm format');
|
|
1049
|
+
errors.push(' • Lint errors: Run pnpm lint:fix');
|
|
1050
|
+
errors.push(' • Type errors: Check pnpm typecheck output');
|
|
1051
|
+
errors.push(' • Audit issues: Check pnpm audit output');
|
|
1052
|
+
errors.push('');
|
|
1053
|
+
errors.push(`After fixing, re-run: pnpm wu:done --id ${id}`);
|
|
1054
|
+
return { valid: false, errors };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* WU-2242: Validate that test_paths is required for non-doc WUs
|
|
1059
|
+
*
|
|
1060
|
+
* Enforces that WUs with code changes (non-documentation types with code_paths
|
|
1061
|
+
* that contain actual code) have at least one test path specified.
|
|
1062
|
+
*
|
|
1063
|
+
* Returns valid: true in the following cases:
|
|
1064
|
+
* - WU type is 'documentation' or 'process'
|
|
1065
|
+
* - code_paths is empty or only contains documentation paths
|
|
1066
|
+
* - tests object has at least one test (unit, e2e, manual, or integration)
|
|
1067
|
+
*
|
|
1068
|
+
* @param {object} wu - WU document
|
|
1069
|
+
* @param {string} wu.id - WU ID
|
|
1070
|
+
* @param {string} wu.type - WU type (feature, bug, documentation, etc.)
|
|
1071
|
+
* @param {object} wu.tests - Tests object with unit, e2e, manual, integration arrays
|
|
1072
|
+
* @param {string[]} wu.code_paths - Array of code paths
|
|
1073
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
1074
|
+
*/
|
|
1075
|
+
export function validateTestPathsRequired(wu) {
|
|
1076
|
+
// Skip validation for documentation and process WUs
|
|
1077
|
+
if (wu.type === WU_TYPES.DOCUMENTATION || wu.type === WU_TYPES.PROCESS) {
|
|
1078
|
+
return { valid: true };
|
|
1079
|
+
}
|
|
1080
|
+
// Skip if code_paths is empty or undefined
|
|
1081
|
+
const codePaths = wu.code_paths || [];
|
|
1082
|
+
if (codePaths.length === 0) {
|
|
1083
|
+
return { valid: true };
|
|
1084
|
+
}
|
|
1085
|
+
// Skip if all code_paths are documentation paths
|
|
1086
|
+
const hasCodeChanges = codePaths.some((p) => !isDocumentationPath(p));
|
|
1087
|
+
if (!hasCodeChanges) {
|
|
1088
|
+
return { valid: true };
|
|
1089
|
+
}
|
|
1090
|
+
// Check if tests object exists and has at least one test
|
|
1091
|
+
const testObj = wu.tests || {};
|
|
1092
|
+
// Helper to check if array has items
|
|
1093
|
+
const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
|
|
1094
|
+
const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
|
|
1095
|
+
const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
|
|
1096
|
+
const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
|
|
1097
|
+
const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
|
|
1098
|
+
// No tests at all - fail
|
|
1099
|
+
if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
|
|
1100
|
+
return {
|
|
1101
|
+
valid: false,
|
|
1102
|
+
error: `${wu.id} requires test_paths: WU has code_paths but no tests specified. Add unit, e2e, integration, or manual tests.`,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
// WU-2332: If we have tests, also check automated test requirement for code files
|
|
1106
|
+
// Manual-only tests are not sufficient for code changes
|
|
1107
|
+
const automatedTestResult = validateAutomatedTestRequirement(wu);
|
|
1108
|
+
if (!automatedTestResult.valid) {
|
|
1109
|
+
// Extract the first error line for the single-error format of this function
|
|
1110
|
+
const errorSummary = automatedTestResult.errors[0]?.split('\n')[0] || 'Automated tests required';
|
|
1111
|
+
return {
|
|
1112
|
+
valid: false,
|
|
1113
|
+
error: `${wu.id}: ${errorSummary}`,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
return { valid: true };
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* WU-2310: Allowed path patterns for documentation WUs.
|
|
1120
|
+
* Mirrors the patterns in gates-pre-commit.mjs gateDocsOnlyPathEnforcement()
|
|
1121
|
+
* to enable early validation at preflight (before transaction starts).
|
|
1122
|
+
*
|
|
1123
|
+
* @constant {RegExp[]}
|
|
1124
|
+
*/
|
|
1125
|
+
const DOCS_ONLY_ALLOWED_PATTERNS = [
|
|
1126
|
+
/^memory-bank\//i,
|
|
1127
|
+
/^docs\//i,
|
|
1128
|
+
/\.md$/i,
|
|
1129
|
+
/^\.beacon\/stamps\//i,
|
|
1130
|
+
/^\.claude\//i,
|
|
1131
|
+
/^ai\//i,
|
|
1132
|
+
/^README\.md$/i,
|
|
1133
|
+
/^CLAUDE\.md$/i,
|
|
1134
|
+
];
|
|
1135
|
+
/**
|
|
1136
|
+
* WU-2310: Check if a path is allowed for documentation WUs.
|
|
1137
|
+
*
|
|
1138
|
+
* @param {string} filePath - File path to check
|
|
1139
|
+
* @returns {boolean} True if path is allowed for docs WUs
|
|
1140
|
+
*/
|
|
1141
|
+
function isAllowedDocsPath(filePath) {
|
|
1142
|
+
if (!filePath || typeof filePath !== 'string')
|
|
1143
|
+
return false;
|
|
1144
|
+
return DOCS_ONLY_ALLOWED_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* WU-2310: Validate type vs code_paths at preflight (before transaction starts).
|
|
1148
|
+
*
|
|
1149
|
+
* This catches the documentation WU + code file mismatch BEFORE any transaction
|
|
1150
|
+
* begins, preventing the scenario where:
|
|
1151
|
+
* 1. Transaction commits files (stamp, status, backlog)
|
|
1152
|
+
* 2. Git commit fails due to pre-commit hook (gateDocsOnlyPathEnforcement)
|
|
1153
|
+
* 3. Files are left in inconsistent state
|
|
1154
|
+
*
|
|
1155
|
+
* By running this validation at preflight, we fail fast with a clear error
|
|
1156
|
+
* message before any file mutations occur.
|
|
1157
|
+
*
|
|
1158
|
+
* @param {object} wu - WU document
|
|
1159
|
+
* @param {string} wu.id - WU ID
|
|
1160
|
+
* @param {string} wu.type - WU type (documentation, feature, bug, etc.)
|
|
1161
|
+
* @param {string[]} [wu.code_paths] - Array of code paths
|
|
1162
|
+
* @returns {{ valid: boolean, errors: string[], blockedPaths: string[], abortedBeforeTransaction: boolean }}
|
|
1163
|
+
*/
|
|
1164
|
+
export function validateTypeVsCodePathsPreflight(wu) {
|
|
1165
|
+
const errors = [];
|
|
1166
|
+
const blockedPaths = [];
|
|
1167
|
+
// Only validate documentation WUs
|
|
1168
|
+
if (wu.type !== WU_TYPES.DOCUMENTATION) {
|
|
1169
|
+
return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
|
|
1170
|
+
}
|
|
1171
|
+
// Skip if no code_paths
|
|
1172
|
+
const codePaths = wu.code_paths;
|
|
1173
|
+
if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
|
|
1174
|
+
return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
|
|
1175
|
+
}
|
|
1176
|
+
// Check each code_path against allowed patterns
|
|
1177
|
+
for (const filePath of codePaths) {
|
|
1178
|
+
if (!isAllowedDocsPath(filePath)) {
|
|
1179
|
+
blockedPaths.push(filePath);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (blockedPaths.length > 0) {
|
|
1183
|
+
const pathsList = blockedPaths.map((p) => ` - ${p}`).join('\n');
|
|
1184
|
+
errors.push(`Documentation WU ${wu.id} has code_paths that would fail pre-commit hook:\n${pathsList}`);
|
|
1185
|
+
return { valid: false, errors, blockedPaths, abortedBeforeTransaction: true };
|
|
1186
|
+
}
|
|
1187
|
+
return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* WU-2310: Build error message for type vs code_paths preflight failure.
|
|
1191
|
+
*
|
|
1192
|
+
* Provides actionable guidance for fixing the mismatch:
|
|
1193
|
+
* 1. Change WU type to 'engineering' or appropriate type
|
|
1194
|
+
* 2. Update code_paths to only include documentation files
|
|
1195
|
+
*
|
|
1196
|
+
* @param {string} id - WU ID
|
|
1197
|
+
* @param {string[]} blockedPaths - Paths that would be blocked
|
|
1198
|
+
* @returns {string} Formatted error message
|
|
1199
|
+
*/
|
|
1200
|
+
export function buildTypeVsCodePathsErrorMessage(id, blockedPaths) {
|
|
1201
|
+
return `
|
|
1202
|
+
PREFLIGHT VALIDATION FAILED (WU-2310)
|
|
1203
|
+
|
|
1204
|
+
WU ${id} is type: documentation but has code_paths that are not allowed:
|
|
1205
|
+
|
|
1206
|
+
${blockedPaths.map((p) => ` - ${p}`).join('\n')}
|
|
1207
|
+
|
|
1208
|
+
This would fail at git commit time (pre-commit hook: gateDocsOnlyPathEnforcement).
|
|
1209
|
+
Aborting BEFORE transaction to prevent inconsistent state.
|
|
1210
|
+
|
|
1211
|
+
Fix options:
|
|
1212
|
+
|
|
1213
|
+
1. Change WU type to 'engineering' (or 'feature', 'bug', etc.):
|
|
1214
|
+
pnpm wu:edit --id ${id} --type engineering
|
|
1215
|
+
|
|
1216
|
+
2. Update code_paths to only include documentation files:
|
|
1217
|
+
pnpm wu:edit --id ${id} --code-paths "docs/..." "*.md"
|
|
1218
|
+
|
|
1219
|
+
Allowed paths for documentation WUs:
|
|
1220
|
+
- docs/
|
|
1221
|
+
- ai/
|
|
1222
|
+
- .claude/
|
|
1223
|
+
- memory-bank/
|
|
1224
|
+
- .beacon/stamps/
|
|
1225
|
+
- *.md files
|
|
1226
|
+
|
|
1227
|
+
After fixing, retry: pnpm wu:done --id ${id}
|
|
1228
|
+
`;
|
|
1229
|
+
}
|