@lumenflow/cli 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 +116 -0
- package/dist/gates.d.ts +41 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +684 -0
- package/dist/gates.js.map +1 -0
- package/dist/initiative-add-wu.d.ts +22 -0
- package/dist/initiative-add-wu.d.ts.map +1 -0
- package/dist/initiative-add-wu.js +234 -0
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-create.d.ts +28 -0
- package/dist/initiative-create.d.ts.map +1 -0
- package/dist/initiative-create.js +172 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.d.ts +34 -0
- package/dist/initiative-edit.d.ts.map +1 -0
- package/dist/initiative-edit.js +440 -0
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.d.ts +12 -0
- package/dist/initiative-list.d.ts.map +1 -0
- package/dist/initiative-list.js +101 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-status.d.ts +11 -0
- package/dist/initiative-status.d.ts.map +1 -0
- package/dist/initiative-status.js +221 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/mem-checkpoint.d.ts +16 -0
- package/dist/mem-checkpoint.d.ts.map +1 -0
- package/dist/mem-checkpoint.js +237 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.d.ts +29 -0
- package/dist/mem-cleanup.d.ts.map +1 -0
- package/dist/mem-cleanup.js +267 -0
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-create.d.ts +17 -0
- package/dist/mem-create.d.ts.map +1 -0
- package/dist/mem-create.js +265 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-inbox.d.ts +35 -0
- package/dist/mem-inbox.d.ts.map +1 -0
- package/dist/mem-inbox.js +373 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-init.d.ts +15 -0
- package/dist/mem-init.d.ts.map +1 -0
- package/dist/mem-init.js +146 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-ready.d.ts +16 -0
- package/dist/mem-ready.d.ts.map +1 -0
- package/dist/mem-ready.js +224 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-signal.d.ts +16 -0
- package/dist/mem-signal.d.ts.map +1 -0
- package/dist/mem-signal.js +204 -0
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.d.ts +16 -0
- package/dist/mem-start.d.ts.map +1 -0
- package/dist/mem-start.js +158 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.d.ts +22 -0
- package/dist/mem-summarize.d.ts.map +1 -0
- package/dist/mem-summarize.js +213 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.d.ts +22 -0
- package/dist/mem-triage.d.ts.map +1 -0
- package/dist/mem-triage.js +328 -0
- package/dist/mem-triage.js.map +1 -0
- package/dist/spawn-list.d.ts +16 -0
- package/dist/spawn-list.d.ts.map +1 -0
- package/dist/spawn-list.js +140 -0
- package/dist/spawn-list.js.map +1 -0
- package/dist/wu-block.d.ts +16 -0
- package/dist/wu-block.d.ts.map +1 -0
- package/dist/wu-block.js +241 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-claim.d.ts +32 -0
- package/dist/wu-claim.d.ts.map +1 -0
- package/dist/wu-claim.js +1106 -0
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup.d.ts +17 -0
- package/dist/wu-cleanup.d.ts.map +1 -0
- package/dist/wu-cleanup.js +194 -0
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-create.d.ts +38 -0
- package/dist/wu-create.d.ts.map +1 -0
- package/dist/wu-create.js +520 -0
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-deps.d.ts +13 -0
- package/dist/wu-deps.d.ts.map +1 -0
- package/dist/wu-deps.js +119 -0
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done.d.ts +153 -0
- package/dist/wu-done.d.ts.map +1 -0
- package/dist/wu-done.js +2096 -0
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.d.ts +29 -0
- package/dist/wu-edit.d.ts.map +1 -0
- package/dist/wu-edit.js +852 -0
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.d.ts +17 -0
- package/dist/wu-infer-lane.d.ts.map +1 -0
- package/dist/wu-infer-lane.js +135 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.d.ts +47 -0
- package/dist/wu-preflight.d.ts.map +1 -0
- package/dist/wu-preflight.js +167 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prune.d.ts +16 -0
- package/dist/wu-prune.d.ts.map +1 -0
- package/dist/wu-prune.js +259 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-repair.d.ts +60 -0
- package/dist/wu-repair.d.ts.map +1 -0
- package/dist/wu-repair.js +226 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-spawn-completion.d.ts +10 -0
- package/dist/wu-spawn-completion.js +30 -0
- package/dist/wu-spawn.d.ts +168 -0
- package/dist/wu-spawn.d.ts.map +1 -0
- package/dist/wu-spawn.js +1327 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-unblock.d.ts +16 -0
- package/dist/wu-unblock.d.ts.map +1 -0
- package/dist/wu-unblock.js +234 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-validate.d.ts +16 -0
- package/dist/wu-validate.d.ts.map +1 -0
- package/dist/wu-validate.js +193 -0
- package/dist/wu-validate.js.map +1 -0
- package/package.json +92 -0
package/dist/wu-done.js
ADDED
|
@@ -0,0 +1,2096 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WU Done Helper
|
|
4
|
+
*
|
|
5
|
+
* Canonical sequence (Worktree mode - DEFAULT):
|
|
6
|
+
* 1) Run gates in lane worktree (validates the change, not just main)
|
|
7
|
+
* 2) Pre-flight validation: run ALL pre-commit hooks before merge (prevents partial completion)
|
|
8
|
+
* 3) cd into worktree
|
|
9
|
+
* 4) Auto-update WU YAML/backlog/status to Done in worktree (unless --no-auto)
|
|
10
|
+
* 5) Create `.beacon/stamps/WU-{id}.done` in worktree
|
|
11
|
+
* 6) Validate staged files against whitelist
|
|
12
|
+
* 7) Commit metadata changes in worktree (on lane branch)
|
|
13
|
+
* 8) cd back to main
|
|
14
|
+
* 9) Merge lane branch to main with --ff-only (metadata + code merged atomically)
|
|
15
|
+
* 10) Push to `main`
|
|
16
|
+
* 11) Remove the associated worktree (unless --no-remove)
|
|
17
|
+
* 12) Optionally delete the lane branch (with --delete-branch)
|
|
18
|
+
* 13) Emit telemetry to .beacon/flow.log
|
|
19
|
+
*
|
|
20
|
+
* Canonical sequence (Branch-Only mode - LEGACY):
|
|
21
|
+
* 1) Run gates on lane branch (in main checkout)
|
|
22
|
+
* 2) Pre-flight validation
|
|
23
|
+
* 3) Merge lane branch to main
|
|
24
|
+
* 4) Update metadata on main
|
|
25
|
+
* 5) Commit and push
|
|
26
|
+
* 6) Delete lane branch
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* pnpm wu:done --id WU-334 [--worktree worktrees/intelligence-wu-334] [--no-auto] [--no-remove] [--no-merge] [--delete-branch]
|
|
30
|
+
*
|
|
31
|
+
* WU-2542: This script imports utilities from @lumenflow/core package.
|
|
32
|
+
* Full migration to thin shim pending @lumenflow/core CLI export implementation.
|
|
33
|
+
*/
|
|
34
|
+
import { execSync } from 'node:child_process';
|
|
35
|
+
import prettyMs from 'pretty-ms';
|
|
36
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
37
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
38
|
+
import { existsSync, readFileSync, mkdirSync, appendFileSync, unlinkSync, statSync } from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
// WU-1825: Import from unified code-path-validator (consolidates 3 validators)
|
|
41
|
+
import { validateWUCodePaths } from '@lumenflow/core/dist/code-path-validator.js';
|
|
42
|
+
// WU-1983: Migration deployment utilities
|
|
43
|
+
import { discoverLocalMigrations, hasMigrationChanges, } from '@lumenflow/core/dist/migration-deployer.js';
|
|
44
|
+
import { validateDocsOnly, getAllowedPathsDescription, } from '@lumenflow/core/dist/docs-path-validator.js';
|
|
45
|
+
import { scanLogForViolations, rotateLog } from '@lumenflow/core/dist/commands-logger.js';
|
|
46
|
+
import { rollbackFiles } from '@lumenflow/core/dist/rollback-utils.js';
|
|
47
|
+
import { validateInputs, detectModeAndPaths, defaultBranchFrom, runCleanup, validateSpecCompleteness, runPreflightTasksValidation, buildPreflightErrorMessage,
|
|
48
|
+
// WU-1805: Preflight code_paths validation before gates
|
|
49
|
+
executePreflightCodePathValidation, buildPreflightCodePathErrorMessage,
|
|
50
|
+
// WU-2308: Pre-commit hooks with worktree context
|
|
51
|
+
validateAllPreCommitHooks,
|
|
52
|
+
// WU-2310: Type vs code_paths preflight validation
|
|
53
|
+
validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from '@lumenflow/core/dist/wu-done-validators.js';
|
|
54
|
+
// WU-1825: validateCodePathsExist moved to unified code-path-validator
|
|
55
|
+
import { validateCodePathsExist } from '@lumenflow/core/dist/code-path-validator.js';
|
|
56
|
+
import { BRANCHES, REMOTES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, PKG_MANAGER, SCRIPTS, CLI_FLAGS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, SKIP_GATES_REASONS, CHECKPOINT_MESSAGES, } from '@lumenflow/core/dist/wu-constants.js';
|
|
57
|
+
import { printGateFailureBox, printStatusPreview } from '@lumenflow/core/dist/wu-done-ui.js';
|
|
58
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
59
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
60
|
+
import { writeWU, appendNote, parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
61
|
+
import { PLACEHOLDER_SENTINEL, validateWU, validateDoneWU, validateApprovalGates, } from '@lumenflow/core/dist/wu-schema.js';
|
|
62
|
+
import { validateBacklogSync } from '@lumenflow/core/dist/backlog-sync-validator.js';
|
|
63
|
+
import { executeBranchOnlyCompletion } from '@lumenflow/core/dist/wu-done-branch-only.js';
|
|
64
|
+
import { executeWorktreeCompletion, autoRebaseBranch, } from '@lumenflow/core/dist/wu-done-worktree.js';
|
|
65
|
+
import { checkWUConsistency } from '@lumenflow/core/dist/wu-consistency-checker.js';
|
|
66
|
+
// WU-1542: Use blocking mode compliance check (replaces non-blocking checkMandatoryAgentsCompliance)
|
|
67
|
+
import { checkMandatoryAgentsComplianceBlocking } from '@lumenflow/core/dist/orchestration-rules.js';
|
|
68
|
+
import { endSessionForWU } from '@lumenflow/agent/dist/auto-session-integration.js';
|
|
69
|
+
import { runBackgroundProcessCheck } from '@lumenflow/core/dist/process-detector.js';
|
|
70
|
+
import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
|
|
71
|
+
// WU-1588: INIT-007 memory layer integration
|
|
72
|
+
import { createCheckpoint } from '@lumenflow/memory/dist/mem-checkpoint-core.js';
|
|
73
|
+
import { createSignal, loadSignals } from '@lumenflow/memory/dist/mem-signal-core.js';
|
|
74
|
+
// WU-1763: Memory store for loading discoveries (lifecycle nudges)
|
|
75
|
+
import { loadMemory, queryByWu } from '@lumenflow/memory/dist/memory-store.js';
|
|
76
|
+
// WU-1943: Checkpoint warning helper
|
|
77
|
+
import { hasSessionCheckpoints } from '@lumenflow/core/dist/wu-done-worktree.js';
|
|
78
|
+
// WU-1603: Atomic lane locking - release lock on WU completion
|
|
79
|
+
import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
|
|
80
|
+
// WU-1747: Checkpoint and lock for concurrent load resilience
|
|
81
|
+
import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, canSkipGates, clearCheckpoint, } from '@lumenflow/core/dist/wu-checkpoint.js';
|
|
82
|
+
// WU-1946: Spawn registry for tracking sub-agent spawns
|
|
83
|
+
import { SpawnRegistryStore } from '@lumenflow/core/dist/spawn-registry-store.js';
|
|
84
|
+
import { SpawnStatus } from '@lumenflow/core/dist/spawn-registry-schema.js';
|
|
85
|
+
// WU-1999: Exposure validation for UI pairing
|
|
86
|
+
// WU-2022: Feature accessibility validation (blocking)
|
|
87
|
+
import { validateExposure, validateFeatureAccessibility, } from '@lumenflow/core/dist/wu-validation.js';
|
|
88
|
+
// WU-1588: Memory layer constants
|
|
89
|
+
const MEMORY_SIGNAL_TYPES = {
|
|
90
|
+
WU_COMPLETION: 'wu_completion',
|
|
91
|
+
};
|
|
92
|
+
const MEMORY_CHECKPOINT_NOTES = {
|
|
93
|
+
PRE_GATES: 'Pre-gates checkpoint for recovery if gates fail',
|
|
94
|
+
};
|
|
95
|
+
const MEMORY_SIGNAL_WINDOW_MS = 60 * 60 * 1000; // 1 hour for recent signals
|
|
96
|
+
// Path constant for wu-events.jsonl (used in multiple places)
|
|
97
|
+
const WU_EVENTS_PATH = '.beacon/state/wu-events.jsonl';
|
|
98
|
+
/**
|
|
99
|
+
* WU-1804: Preflight validation for claim metadata before gates.
|
|
100
|
+
*
|
|
101
|
+
* Validates that the WU is properly claimed before running gates:
|
|
102
|
+
* 1. Worktree YAML status must be 'in_progress'
|
|
103
|
+
* 2. State store must show WU as 'in_progress'
|
|
104
|
+
*
|
|
105
|
+
* If either fails, exits before gates with actionable guidance to run wu:repair-claim.
|
|
106
|
+
* This prevents burning tokens on gates that will ultimately fail.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} id - WU ID
|
|
109
|
+
* @param {string} worktreePath - Path to the worktree
|
|
110
|
+
* @param {string} yamlStatus - Current status from worktree YAML
|
|
111
|
+
* @returns {Promise<void>}
|
|
112
|
+
*/
|
|
113
|
+
async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
|
|
114
|
+
const errors = [];
|
|
115
|
+
// Check 1: YAML status must be in_progress
|
|
116
|
+
if (yamlStatus !== WU_STATUS.IN_PROGRESS) {
|
|
117
|
+
errors.push(`Worktree YAML status is '${yamlStatus}', expected '${WU_STATUS.IN_PROGRESS}'`);
|
|
118
|
+
}
|
|
119
|
+
// Check 2: State store must show WU as in_progress
|
|
120
|
+
const resolvedWorktreePath = path.resolve(worktreePath);
|
|
121
|
+
const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
|
|
122
|
+
const eventsPath = path.join(resolvedWorktreePath, WU_EVENTS_PATH);
|
|
123
|
+
try {
|
|
124
|
+
const store = new WUStateStore(stateDir);
|
|
125
|
+
await store.load();
|
|
126
|
+
const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
|
|
127
|
+
if (!inProgress.has(id)) {
|
|
128
|
+
errors.push(`State store does not show ${id} as in_progress (path: ${eventsPath})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
errors.push(`Cannot read state store: ${err.message} (path: ${eventsPath})`);
|
|
133
|
+
}
|
|
134
|
+
// If no errors, we're good
|
|
135
|
+
if (errors.length === 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Build actionable error message with wu:repair-claim guidance
|
|
139
|
+
die(`❌ CLAIM METADATA VALIDATION FAILED (WU-1804)\n\n` +
|
|
140
|
+
`Cannot proceed with wu:done - the WU is not properly claimed.\n\n` +
|
|
141
|
+
`Issues detected:\n${errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
|
|
142
|
+
`This typically happens when:\n` +
|
|
143
|
+
` • A crash/rebase interrupted worktree creation\n` +
|
|
144
|
+
` • The claim transaction was partially completed\n` +
|
|
145
|
+
` • Another process modified the WU state\n\n` +
|
|
146
|
+
`Next step:\n` +
|
|
147
|
+
` pnpm wu:repair-claim --id ${id}\n\n` +
|
|
148
|
+
`After repair, retry:\n` +
|
|
149
|
+
` pnpm wu:done --id ${id}\n\n` +
|
|
150
|
+
`See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.`);
|
|
151
|
+
}
|
|
152
|
+
export function printExposureWarnings(wu, options = {}) {
|
|
153
|
+
// Validate exposure
|
|
154
|
+
const result = validateExposure(wu, { skipExposureCheck: options.skipExposureCheck });
|
|
155
|
+
// Print warnings if any
|
|
156
|
+
if (result.warnings.length > 0) {
|
|
157
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1999: Exposure validation warnings:`);
|
|
158
|
+
for (const warning of result.warnings) {
|
|
159
|
+
console.log(`${LOG_PREFIX.DONE} ${warning}`);
|
|
160
|
+
}
|
|
161
|
+
console.log(`${LOG_PREFIX.DONE} These are non-blocking warnings. ` +
|
|
162
|
+
`To skip, use --skip-exposure-check flag.\n`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function validateAccessibilityOrDie(wu, options = {}) {
|
|
166
|
+
const result = validateFeatureAccessibility(wu, {
|
|
167
|
+
skipAccessibilityCheck: options.skipAccessibilityCheck,
|
|
168
|
+
});
|
|
169
|
+
if (!result.valid) {
|
|
170
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} WU-2022: Feature accessibility validation failed`);
|
|
171
|
+
die(`❌ FEATURE ACCESSIBILITY VALIDATION FAILED (WU-2022)\n\n` +
|
|
172
|
+
`Cannot complete wu:done - UI feature accessibility not verified.\n\n` +
|
|
173
|
+
`${result.errors.join('\n\n')}\n\n` +
|
|
174
|
+
`This gate prevents "orphaned code" - features that exist but users cannot access.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function assertWorktreeWUInProgressInStateStore(id, worktreePath) {
|
|
178
|
+
const resolvedWorktreePath = path.resolve(worktreePath);
|
|
179
|
+
const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
|
|
180
|
+
const eventsPath = path.join(resolvedWorktreePath, WU_EVENTS_PATH);
|
|
181
|
+
const store = new WUStateStore(stateDir);
|
|
182
|
+
try {
|
|
183
|
+
await store.load();
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
die(`Cannot read WU state store for ${id}.\n\n` +
|
|
187
|
+
`Path: ${eventsPath}\n\n` +
|
|
188
|
+
`Error: ${err.message}\n\n` +
|
|
189
|
+
`If this WU was claimed on an older tool version or the event log is missing/corrupt,\n` +
|
|
190
|
+
`repair the worktree state store before rerunning wu:done.`);
|
|
191
|
+
}
|
|
192
|
+
const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
|
|
193
|
+
if (!inProgress.has(id)) {
|
|
194
|
+
die(`WU ${id} is not in_progress in the worktree state store.\n\n` +
|
|
195
|
+
`Path: ${eventsPath}\n\n` +
|
|
196
|
+
`This will fail later when wu:done tries to append a complete event and regenerate backlog/status.\n` +
|
|
197
|
+
`Fix the claim/state log first, then rerun wu:done.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* WU-1588: Create pre-gates checkpoint for recovery if gates fail.
|
|
202
|
+
* Non-blocking wrapper around mem:checkpoint - failures logged as warnings.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} id - WU ID
|
|
205
|
+
* @param {string|null} worktreePath - Path to worktree
|
|
206
|
+
* @param {string} baseDir - Base directory for memory layer
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
async function createPreGatesCheckpoint(id, worktreePath, baseDir = process.cwd()) {
|
|
210
|
+
try {
|
|
211
|
+
const result = await createCheckpoint(baseDir, {
|
|
212
|
+
note: MEMORY_CHECKPOINT_NOTES.PRE_GATES,
|
|
213
|
+
wuId: id,
|
|
214
|
+
progress: `Starting gates execution for ${id}`,
|
|
215
|
+
nextSteps: worktreePath
|
|
216
|
+
? `Gates running in worktree: ${worktreePath}`
|
|
217
|
+
: 'Gates running in branch-only mode',
|
|
218
|
+
trigger: 'wu-done-pre-gates',
|
|
219
|
+
});
|
|
220
|
+
if (result.success) {
|
|
221
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Pre-gates checkpoint created (${result.checkpoint.id})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
// Non-blocking: checkpoint failure should not block wu:done
|
|
226
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not create pre-gates checkpoint: ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* WU-1588: Broadcast completion signal to parallel agents.
|
|
231
|
+
* Non-blocking wrapper around mem:signal - failures logged as warnings.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} id - WU ID
|
|
234
|
+
* @param {string} title - WU title
|
|
235
|
+
* @param {string} baseDir - Base directory for memory layer
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async function broadcastCompletionSignal(id, title, baseDir = process.cwd()) {
|
|
239
|
+
try {
|
|
240
|
+
const result = await createSignal(baseDir, {
|
|
241
|
+
message: `${MEMORY_SIGNAL_TYPES.WU_COMPLETION}: ${id} - ${title}`,
|
|
242
|
+
wuId: id,
|
|
243
|
+
});
|
|
244
|
+
if (result.success) {
|
|
245
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Completion signal broadcast (${result.signal.id})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
// Non-blocking: signal failure should not block wu:done
|
|
250
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not broadcast completion signal: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* WU-1588: Check inbox for recent signals from parallel agents.
|
|
255
|
+
* Non-blocking wrapper around loadSignals - failures logged as warnings.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} id - Current WU ID (for filtering)
|
|
258
|
+
* @param {string} baseDir - Base directory for memory layer
|
|
259
|
+
* @returns {Promise<void>}
|
|
260
|
+
*/
|
|
261
|
+
async function checkInboxForRecentSignals(id, baseDir = process.cwd()) {
|
|
262
|
+
try {
|
|
263
|
+
const since = new Date(Date.now() - MEMORY_SIGNAL_WINDOW_MS);
|
|
264
|
+
const signals = await loadSignals(baseDir, { since, unreadOnly: true });
|
|
265
|
+
// Filter out signals for current WU
|
|
266
|
+
const relevantSignals = signals.filter((s) => s.wu_id !== id);
|
|
267
|
+
if (relevantSignals.length > 0) {
|
|
268
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.INFO} Recent signals from parallel agents:`);
|
|
269
|
+
for (const signal of relevantSignals.slice(0, 5)) {
|
|
270
|
+
// Show at most 5
|
|
271
|
+
const timestamp = new Date(signal.created_at).toLocaleTimeString();
|
|
272
|
+
console.log(` - [${timestamp}] ${signal.message}`);
|
|
273
|
+
}
|
|
274
|
+
if (relevantSignals.length > 5) {
|
|
275
|
+
console.log(` ... and ${relevantSignals.length - 5} more`);
|
|
276
|
+
}
|
|
277
|
+
console.log(` Run 'pnpm mem:inbox' for full list\n`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
// Non-blocking: inbox check failure should not block wu:done
|
|
282
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check inbox for signals: ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* WU-1946: Update spawn registry on WU completion.
|
|
287
|
+
* Non-blocking wrapper - failures logged as warnings.
|
|
288
|
+
*
|
|
289
|
+
* When a WU is completed via wu:done, this function updates the spawn registry
|
|
290
|
+
* to mark the spawned entry as completed (if one exists). This allows orchestrators
|
|
291
|
+
* to track sub-agent spawn completion status.
|
|
292
|
+
*
|
|
293
|
+
* Gracefully skips if:
|
|
294
|
+
* - No spawn entry found for this WU (legacy WU created before registry)
|
|
295
|
+
* - Registry file doesn't exist
|
|
296
|
+
* - Any error during update
|
|
297
|
+
*
|
|
298
|
+
* @param {string} id - WU ID being completed
|
|
299
|
+
* @param {string} baseDir - Base directory containing .beacon/state/
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd()) {
|
|
303
|
+
try {
|
|
304
|
+
const store = new SpawnRegistryStore(path.join(baseDir, '.beacon', 'state'));
|
|
305
|
+
await store.load();
|
|
306
|
+
const spawnEntry = store.getByTarget(id);
|
|
307
|
+
// Graceful skip if no spawn entry found (legacy WU)
|
|
308
|
+
if (!spawnEntry) {
|
|
309
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No spawn registry entry found for ${id} (legacy WU or not spawned)`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Update status to completed with completedAt timestamp
|
|
313
|
+
await store.updateStatus(spawnEntry.id, SpawnStatus.COMPLETED);
|
|
314
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spawn registry updated: ${id} marked as completed`);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
// Non-blocking: spawn registry update failure should not block wu:done
|
|
318
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not update spawn registry for ${id}: ${err.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Git config keys used for user identification
|
|
322
|
+
const GIT_CONFIG_USER_NAME = 'user.name';
|
|
323
|
+
const GIT_CONFIG_USER_EMAIL = 'user.email';
|
|
324
|
+
// Default fallback messages
|
|
325
|
+
const DEFAULT_NO_REASON = '(no reason provided)';
|
|
326
|
+
/**
|
|
327
|
+
* WU-1234: Normalize username for ownership comparison
|
|
328
|
+
* Extracts username from email address for comparison.
|
|
329
|
+
* This allows tom@hellm.ai to match 'tom' assigned_to field.
|
|
330
|
+
*
|
|
331
|
+
* @param {string|null|undefined} value - Email address or username
|
|
332
|
+
* @returns {string} Normalized username (lowercase)
|
|
333
|
+
*/
|
|
334
|
+
export function normalizeUsername(value) {
|
|
335
|
+
if (!value)
|
|
336
|
+
return '';
|
|
337
|
+
const str = String(value).trim();
|
|
338
|
+
// Extract username from email: tom@hellm.ai -> tom
|
|
339
|
+
// WU-1281: Using string split instead of regex
|
|
340
|
+
const atIndex = str.indexOf('@');
|
|
341
|
+
const username = atIndex > 0 ? str.slice(0, atIndex) : str;
|
|
342
|
+
return username.toLowerCase();
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* WU-1234: Detect if branch is already merged to main
|
|
346
|
+
* Checks if branch tip is an ancestor of main HEAD (i.e., already merged).
|
|
347
|
+
* This prevents merge loops when code was merged via emergency fix or manual merge.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} branch - Lane branch name
|
|
350
|
+
* @returns {Promise<boolean>} True if branch is already merged to main
|
|
351
|
+
*/
|
|
352
|
+
export async function isBranchAlreadyMerged(branch) {
|
|
353
|
+
try {
|
|
354
|
+
const gitAdapter = getGitForCwd();
|
|
355
|
+
const branchTip = (await gitAdapter.getCommitHash(branch)).trim();
|
|
356
|
+
const mergeBase = (await gitAdapter.mergeBase(BRANCHES.MAIN, branch)).trim();
|
|
357
|
+
const mainHead = (await gitAdapter.getCommitHash(BRANCHES.MAIN)).trim();
|
|
358
|
+
// Branch is already merged if:
|
|
359
|
+
// 1. Branch tip equals merge-base (branch has been rebased/merged onto main)
|
|
360
|
+
// 2. Branch tip is an ancestor of main HEAD
|
|
361
|
+
if (branchTip === mergeBase) {
|
|
362
|
+
// Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
|
|
363
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Branch ${branch} is already merged to main\n` +
|
|
364
|
+
` Branch tip: ${branchTip.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
|
|
365
|
+
` Merge-base: ${mergeBase.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
|
|
366
|
+
` Main HEAD: ${mainHead.substring(0, GIT.SHA_SHORT_LENGTH)}`);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
console.warn(`${LOG_PREFIX.DONE} Could not check if branch is already merged: ${e.message}`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// WU-1281: isDocsOnlyByPaths removed - use shouldSkipWebTests from path-classifiers.mjs
|
|
377
|
+
// The validators already use shouldSkipWebTests via detectDocsOnlyByPaths wrapper.
|
|
378
|
+
// Keeping the export for backward compatibility but re-exporting the canonical function.
|
|
379
|
+
export { shouldSkipWebTests as isDocsOnlyByPaths } from '@lumenflow/core/dist/path-classifiers.js';
|
|
380
|
+
/**
|
|
381
|
+
* WU-1234: Pre-flight check for backlog state consistency
|
|
382
|
+
* Fails fast if the WU appears in both Done and In Progress sections.
|
|
383
|
+
*
|
|
384
|
+
* @param {string} id - WU ID to check
|
|
385
|
+
* @param {string} backlogPath - Path to backlog.md
|
|
386
|
+
* @returns {{ valid: boolean, error: string|null }}
|
|
387
|
+
*/
|
|
388
|
+
export function checkBacklogConsistencyForWU(id, backlogPath) {
|
|
389
|
+
try {
|
|
390
|
+
const result = validateBacklogSync(backlogPath);
|
|
391
|
+
// Check if this specific WU is in both Done and In Progress
|
|
392
|
+
if (!result.valid) {
|
|
393
|
+
for (const error of result.errors) {
|
|
394
|
+
// Check if the error mentions both Done and In Progress AND mentions our WU
|
|
395
|
+
if (error.includes('Done and In Progress') && error.includes(id)) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
error: `❌ BACKLOG STATE INCONSISTENCY: ${id} found in both Done and In Progress sections.\n\n` +
|
|
399
|
+
`This is an invalid state that must be fixed manually before wu:done can proceed.\n\n` +
|
|
400
|
+
`Fix options:\n` +
|
|
401
|
+
` 1. If ${id} is truly done: Remove from In Progress in backlog.md\n` +
|
|
402
|
+
` 2. If ${id} needs more work: Remove from Done in backlog.md, update WU YAML status\n\n` +
|
|
403
|
+
`After fixing backlog.md, retry: pnpm wu:done --id ${id}`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { valid: true, error: null };
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
// If validation fails (e.g., file not found), warn but don't block
|
|
412
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not validate backlog consistency: ${e.message}`);
|
|
413
|
+
return { valid: true, error: null };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Read commitlint header-max-length from config, fallback to DEFAULTS.MAX_COMMIT_SUBJECT
|
|
418
|
+
* WU-1281: Using centralized constant instead of hardcoded 100
|
|
419
|
+
*/
|
|
420
|
+
function getCommitHeaderLimit() {
|
|
421
|
+
try {
|
|
422
|
+
const configPath = path.join(process.cwd(), '.commitlintrc.json');
|
|
423
|
+
if (!existsSync(configPath))
|
|
424
|
+
return DEFAULTS.MAX_COMMIT_SUBJECT;
|
|
425
|
+
const cfg = JSON.parse(readFileSync(configPath, { encoding: FILE_SYSTEM.UTF8 }));
|
|
426
|
+
return cfg?.rules?.['header-max-length']?.[2] ?? DEFAULTS.MAX_COMMIT_SUBJECT;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return DEFAULTS.MAX_COMMIT_SUBJECT; // Fallback if config is malformed or missing
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// ensureOnMain() moved to wu-helpers.mjs (WU-1256)
|
|
433
|
+
/**
|
|
434
|
+
* Ensure working tree is clean before wu:done operations.
|
|
435
|
+
*
|
|
436
|
+
* Prevents multi-agent data loss: If uncommitted files exist in main
|
|
437
|
+
* checkout, wu:done operations may fail mid-workflow. Agents then
|
|
438
|
+
* automatically "clean up" by running git reset/clean, destroying
|
|
439
|
+
* other agents' uncommitted work.
|
|
440
|
+
*
|
|
441
|
+
* This check HALTS wu:done immediately and guides the agent to verify
|
|
442
|
+
* ownership before proceeding.
|
|
443
|
+
*
|
|
444
|
+
* Context: WU-635 (multi-agent coordination)
|
|
445
|
+
* See: CLAUDE.md §2.2
|
|
446
|
+
*/
|
|
447
|
+
async function ensureCleanWorkingTree() {
|
|
448
|
+
const status = await getGitForCwd().getStatus();
|
|
449
|
+
if (status.trim()) {
|
|
450
|
+
die(`Working tree is not clean. Cannot proceed with wu:done.\n\n` +
|
|
451
|
+
`Uncommitted changes in main checkout:\n${status}\n\n` +
|
|
452
|
+
`⚠️ CRITICAL: These may be another agent's work!\n\n` +
|
|
453
|
+
`Before proceeding:\n` +
|
|
454
|
+
`1. Check if these are YOUR changes (forgot to commit in main)\n` +
|
|
455
|
+
` → If yes: Commit them now, then retry wu:done\n\n` +
|
|
456
|
+
`2. Check if these are ANOTHER AGENT's changes\n` +
|
|
457
|
+
` → If yes: STOP. Coordinate with user before proceeding\n` +
|
|
458
|
+
` → NEVER remove another agent's uncommitted work\n\n` +
|
|
459
|
+
`Multi-agent coordination: See CLAUDE.md §2.2\n\n` +
|
|
460
|
+
`Common causes:\n` +
|
|
461
|
+
` - You forgot to commit changes before claiming a different WU\n` +
|
|
462
|
+
` - Another agent is actively working in main checkout\n` +
|
|
463
|
+
` - Leftover changes from previous session`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Extract completed WU IDs from git log output.
|
|
468
|
+
* @param {string} logOutput - Git log output (one commit per line)
|
|
469
|
+
* @param {string} currentId - Current WU ID to exclude
|
|
470
|
+
* @returns {string[]} Array of completed WU IDs
|
|
471
|
+
*/
|
|
472
|
+
function extractCompletedWUIds(logOutput, currentId) {
|
|
473
|
+
const wuPattern = /wu\((wu-\d+)\):/gi;
|
|
474
|
+
const seenIds = new Set();
|
|
475
|
+
const completedWUs = [];
|
|
476
|
+
for (const line of logOutput.split(STRING_LITERALS.NEWLINE)) {
|
|
477
|
+
// Only process "done" commits
|
|
478
|
+
if (!line.toLowerCase().includes('done'))
|
|
479
|
+
continue;
|
|
480
|
+
let match;
|
|
481
|
+
while ((match = wuPattern.exec(line)) !== null) {
|
|
482
|
+
const wuId = match[1].toUpperCase();
|
|
483
|
+
// Skip current WU and duplicates
|
|
484
|
+
if (wuId !== currentId && !seenIds.has(wuId)) {
|
|
485
|
+
seenIds.add(wuId);
|
|
486
|
+
completedWUs.push(wuId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return completedWUs;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Build warning message for parallel completions.
|
|
494
|
+
*/
|
|
495
|
+
function buildParallelWarning(id, completedWUs, baselineSha, currentSha) {
|
|
496
|
+
const wuList = completedWUs.map((wu) => ` • ${wu}`).join(STRING_LITERALS.NEWLINE);
|
|
497
|
+
return `
|
|
498
|
+
${EMOJI.WARNING} PARALLEL COMPLETIONS DETECTED ${EMOJI.WARNING}
|
|
499
|
+
|
|
500
|
+
The following WUs were completed and merged to main since you claimed ${id}:
|
|
501
|
+
|
|
502
|
+
${wuList}
|
|
503
|
+
|
|
504
|
+
This may cause rebase conflicts when wu:done attempts to merge.
|
|
505
|
+
|
|
506
|
+
Options:
|
|
507
|
+
1. Proceed anyway - rebase will attempt to resolve conflicts
|
|
508
|
+
2. Abort and manually rebase: git fetch origin main && git rebase origin/main
|
|
509
|
+
3. Check if any completed WUs touched the same files
|
|
510
|
+
|
|
511
|
+
Baseline: ${baselineSha.substring(0, 8)}
|
|
512
|
+
Current: ${currentSha.substring(0, 8)}
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* WU-1382: Detect parallel WU completions since claim time.
|
|
517
|
+
*
|
|
518
|
+
* When multiple agents work in parallel, one may complete a WU and merge to main
|
|
519
|
+
* while another is still working. This function detects such completions early,
|
|
520
|
+
* before wu:done attempts the merge, allowing the agent to decide whether to
|
|
521
|
+
* proceed (with potential rebase conflicts) or abort.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} id - Current WU ID
|
|
524
|
+
* @param {object} doc - WU YAML document (from worktree or main)
|
|
525
|
+
* @returns {Promise<{hasParallelCompletions: boolean, completedWUs: string[], warning: string|null}>}
|
|
526
|
+
*/
|
|
527
|
+
async function detectParallelCompletions(id, doc) {
|
|
528
|
+
const noParallel = { hasParallelCompletions: false, completedWUs: [], warning: null };
|
|
529
|
+
const baselineSha = doc.baseline_main_sha;
|
|
530
|
+
// If no baseline recorded (legacy WU), skip detection
|
|
531
|
+
if (!baselineSha) {
|
|
532
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No baseline_main_sha recorded (legacy WU) - skipping parallel detection`);
|
|
533
|
+
return noParallel;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const gitAdapter = getGitForCwd();
|
|
537
|
+
await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
538
|
+
const currentSha = (await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`)).trim();
|
|
539
|
+
if (currentSha === baselineSha) {
|
|
540
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} No parallel completions detected (main unchanged since claim)`);
|
|
541
|
+
return noParallel;
|
|
542
|
+
}
|
|
543
|
+
const logOutput = await gitAdapter.raw([
|
|
544
|
+
'log',
|
|
545
|
+
'--oneline',
|
|
546
|
+
'--grep=^wu(wu-',
|
|
547
|
+
`${baselineSha}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
|
|
548
|
+
]);
|
|
549
|
+
if (!logOutput?.trim()) {
|
|
550
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no WU completions detected`);
|
|
551
|
+
return noParallel;
|
|
552
|
+
}
|
|
553
|
+
const completedWUs = extractCompletedWUIds(logOutput, id);
|
|
554
|
+
if (completedWUs.length === 0) {
|
|
555
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no other WU completions`);
|
|
556
|
+
return noParallel;
|
|
557
|
+
}
|
|
558
|
+
const warning = buildParallelWarning(id, completedWUs, baselineSha, currentSha);
|
|
559
|
+
return { hasParallelCompletions: true, completedWUs, warning };
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not detect parallel completions: ${err.message}`);
|
|
563
|
+
return noParallel;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Ensure main branch is up-to-date with origin before merge operations.
|
|
568
|
+
*
|
|
569
|
+
* Prevents coordination failures when Agent A pushes to main while Agent B
|
|
570
|
+
* is working. Without this check, Agent B's wu:done would fail with cryptic
|
|
571
|
+
* fast-forward errors when trying to merge.
|
|
572
|
+
*
|
|
573
|
+
* Context: WU-705 (fix agent coordination failures)
|
|
574
|
+
* See: CLAUDE.md §2.7
|
|
575
|
+
*/
|
|
576
|
+
async function ensureMainUpToDate() {
|
|
577
|
+
console.log(`${LOG_PREFIX.DONE} Checking if main is up-to-date with origin...`);
|
|
578
|
+
try {
|
|
579
|
+
// Fetch latest without merging
|
|
580
|
+
const gitAdapter = getGitForCwd();
|
|
581
|
+
await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
582
|
+
const localMain = await gitAdapter.getCommitHash(BRANCHES.MAIN);
|
|
583
|
+
const remoteMain = await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
|
|
584
|
+
if (localMain !== remoteMain) {
|
|
585
|
+
const behind = await gitAdapter.revList([
|
|
586
|
+
'--count',
|
|
587
|
+
`${BRANCHES.MAIN}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
|
|
588
|
+
]);
|
|
589
|
+
const ahead = await gitAdapter.revList([
|
|
590
|
+
'--count',
|
|
591
|
+
`${REMOTES.ORIGIN}/${BRANCHES.MAIN}..${BRANCHES.MAIN}`,
|
|
592
|
+
]);
|
|
593
|
+
die(`Main branch is out of sync with ${REMOTES.ORIGIN}.\n\n` +
|
|
594
|
+
`Local ${BRANCHES.MAIN} is ${behind} commits behind and ${ahead} commits ahead of ${REMOTES.ORIGIN}/${BRANCHES.MAIN}.\n\n` +
|
|
595
|
+
`Update main before running wu:done:\n` +
|
|
596
|
+
` git pull origin main\n` +
|
|
597
|
+
` # Then retry:\n` +
|
|
598
|
+
` pnpm wu:done --id ${process.argv.find((a) => a.startsWith('WU-')) || 'WU-XXX'}\n\n` +
|
|
599
|
+
`This prevents fast-forward merge failures during wu:done completion.\n\n` +
|
|
600
|
+
`Why this happens:\n` +
|
|
601
|
+
` - Another agent completed a WU and pushed to main\n` +
|
|
602
|
+
` - Your main checkout is now behind origin/main\n` +
|
|
603
|
+
` - The fast-forward merge will fail without updating first\n\n` +
|
|
604
|
+
`Multi-agent coordination: See CLAUDE.md §2.7`);
|
|
605
|
+
}
|
|
606
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main is up-to-date with origin`);
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not verify main sync: ${err.message}`);
|
|
610
|
+
console.warn(`${LOG_PREFIX.DONE} Proceeding anyway (network issue or no remote)`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Tripwire check: Scan commands log for violations (WU-630 detective layer)
|
|
615
|
+
*
|
|
616
|
+
* Scans .beacon/commands.log for destructive git commands executed during
|
|
617
|
+
* this agent session. If violations are found, aborts wu:done and displays
|
|
618
|
+
* remediation guidance.
|
|
619
|
+
*
|
|
620
|
+
* This is defense-in-depth: catches violations even if git shim was bypassed
|
|
621
|
+
* by calling /usr/bin/git directly or if PATH was not set up correctly.
|
|
622
|
+
*
|
|
623
|
+
* Context: WU-630 (detective layer, Layer 3 of 4)
|
|
624
|
+
* See: docs/04-operations/_frameworks/lumenflow/02-playbook.md §4.6
|
|
625
|
+
*/
|
|
626
|
+
function runTripwireCheck() {
|
|
627
|
+
const violations = scanLogForViolations();
|
|
628
|
+
if (violations.length === 0) {
|
|
629
|
+
return; // All clear
|
|
630
|
+
}
|
|
631
|
+
// Violations detected - format error message with remediation
|
|
632
|
+
console.error('\n⛔ VIOLATION DETECTED: Destructive Git Commands on Main\n');
|
|
633
|
+
console.error('The following forbidden git commands were executed during this session:\n');
|
|
634
|
+
violations.forEach((v, i) => {
|
|
635
|
+
console.error(` ${i + 1}. ${v.command}`);
|
|
636
|
+
console.error(` Branch: ${v.branch}`);
|
|
637
|
+
console.error(` Worktree: ${v.worktree}`);
|
|
638
|
+
console.error(` Time: ${v.timestamp}\n`);
|
|
639
|
+
});
|
|
640
|
+
console.error(`\nTotal: ${violations.length} violations\n`);
|
|
641
|
+
// Remediation guidance based on violation type
|
|
642
|
+
console.error("⚠️ CRITICAL: These commands may have destroyed other agents' work!\n");
|
|
643
|
+
console.error('Remediation Steps:\n');
|
|
644
|
+
const hasReset = violations.some((v) => v.command.includes('reset --hard'));
|
|
645
|
+
const hasStash = violations.some((v) => v.command.includes('stash'));
|
|
646
|
+
const hasClean = violations.some((v) => v.command.includes('clean'));
|
|
647
|
+
if (hasReset) {
|
|
648
|
+
console.error('📋 git reset --hard detected:');
|
|
649
|
+
console.error(' 1. Check git reflog to recover lost commits:');
|
|
650
|
+
console.error(' git reflog');
|
|
651
|
+
console.error(' git reset --hard HEAD@{N} (where N is the commit before reset)');
|
|
652
|
+
console.error(' 2. If reflog shows lost work, restore it immediately\n');
|
|
653
|
+
}
|
|
654
|
+
if (hasStash) {
|
|
655
|
+
console.error('📋 git stash detected:');
|
|
656
|
+
console.error(" 1. Check if stash contains other agents' work:");
|
|
657
|
+
console.error(' git stash list');
|
|
658
|
+
console.error(' git stash show -p stash@{0}');
|
|
659
|
+
console.error(' 2. If stash contains work, pop it back:');
|
|
660
|
+
console.error(' git stash pop\n');
|
|
661
|
+
}
|
|
662
|
+
if (hasClean) {
|
|
663
|
+
console.error('📋 git clean detected:');
|
|
664
|
+
console.error(' 1. Deleted files may not be recoverable');
|
|
665
|
+
console.error(' 2. Check git status for any remaining untracked files');
|
|
666
|
+
console.error(' 3. Escalate to human if critical files were deleted\n');
|
|
667
|
+
}
|
|
668
|
+
console.error('📖 See detailed recovery steps:');
|
|
669
|
+
console.error(' docs/04-operations/_frameworks/lumenflow/02-playbook.md §4.6\n');
|
|
670
|
+
console.error('🚫 DO NOT proceed with wu:done until violations are remediated.\n');
|
|
671
|
+
console.error('Fix violations first, then retry wu:done.\n');
|
|
672
|
+
// Also rotate log (cleanup old entries)
|
|
673
|
+
rotateLog();
|
|
674
|
+
process.exit(EXIT_CODES.ERROR);
|
|
675
|
+
}
|
|
676
|
+
async function listStaged() {
|
|
677
|
+
// WU-1235: Use getGitForCwd() to capture current directory (worktree after chdir)
|
|
678
|
+
const gitCwd = getGitForCwd();
|
|
679
|
+
const raw = await gitCwd.raw(['diff', '--cached', '--name-only']);
|
|
680
|
+
return raw ? raw.split(/\r?\n/).filter(Boolean) : [];
|
|
681
|
+
}
|
|
682
|
+
// In --no-auto mode, allow a safe no-op: if NONE of the expected files are staged,
|
|
683
|
+
// treat as already-synchronised and continue. If SOME are staged and SOME missing,
|
|
684
|
+
// still fail with guidance.
|
|
685
|
+
async function ensureNoAutoStagedOrNoop(paths) {
|
|
686
|
+
const staged = await listStaged();
|
|
687
|
+
const isStaged = (p) => staged.some((name) => name === p || name.startsWith(`${p}/`));
|
|
688
|
+
const present = paths.filter(Boolean).filter((p) => isStaged(p));
|
|
689
|
+
if (present.length === 0) {
|
|
690
|
+
console.log(`${LOG_PREFIX.DONE} No staged changes detected for --no-auto; treating as no-op finalisation (repo already in done state)`);
|
|
691
|
+
return { noop: true };
|
|
692
|
+
}
|
|
693
|
+
const missing = paths.filter(Boolean).filter((p) => !isStaged(p));
|
|
694
|
+
if (missing.length > 0) {
|
|
695
|
+
die(`Stage updates for: ${missing.join(', ')}`);
|
|
696
|
+
}
|
|
697
|
+
return { noop: false };
|
|
698
|
+
}
|
|
699
|
+
export function emitTelemetry(event) {
|
|
700
|
+
const logPath = path.join('.beacon', 'flow.log');
|
|
701
|
+
const logDir = path.dirname(logPath);
|
|
702
|
+
if (!existsSync(logDir))
|
|
703
|
+
mkdirSync(logDir, { recursive: true });
|
|
704
|
+
const line = JSON.stringify({ timestamp: new Date().toISOString(), ...event });
|
|
705
|
+
appendFileSync(logPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
706
|
+
}
|
|
707
|
+
async function auditSkipGates(id, reason, fixWU, worktreePath) {
|
|
708
|
+
const auditPath = path.join('.beacon', 'skip-gates-audit.log');
|
|
709
|
+
const auditDir = path.dirname(auditPath);
|
|
710
|
+
if (!existsSync(auditDir))
|
|
711
|
+
mkdirSync(auditDir, { recursive: true });
|
|
712
|
+
const gitAdapter = getGitForCwd();
|
|
713
|
+
const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
|
|
714
|
+
const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
|
|
715
|
+
const commitHash = await gitAdapter.getCommitHash();
|
|
716
|
+
const entry = {
|
|
717
|
+
timestamp: new Date().toISOString(),
|
|
718
|
+
wu_id: id,
|
|
719
|
+
reason: reason || DEFAULT_NO_REASON,
|
|
720
|
+
fix_wu: fixWU || '(no fix WU specified)',
|
|
721
|
+
worktree: worktreePath || '(unknown)',
|
|
722
|
+
git_user: `${userName.trim()} <${userEmail.trim()}>`,
|
|
723
|
+
git_commit: commitHash.trim(),
|
|
724
|
+
};
|
|
725
|
+
const line = JSON.stringify(entry);
|
|
726
|
+
appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
727
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-gates event logged to ${auditPath}`);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Audit trail for --skip-cos-gates (COS v1.3 §7)
|
|
731
|
+
*/
|
|
732
|
+
async function auditSkipCosGates(id, reason) {
|
|
733
|
+
const auditPath = path.join('.beacon', 'skip-cos-gates-audit.log');
|
|
734
|
+
const auditDir = path.dirname(auditPath);
|
|
735
|
+
if (!existsSync(auditDir))
|
|
736
|
+
mkdirSync(auditDir, { recursive: true });
|
|
737
|
+
const gitAdapter = getGitForCwd();
|
|
738
|
+
const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
|
|
739
|
+
const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
|
|
740
|
+
const commitHash = await gitAdapter.getCommitHash();
|
|
741
|
+
const entry = {
|
|
742
|
+
timestamp: new Date().toISOString(),
|
|
743
|
+
wu_id: id,
|
|
744
|
+
reason: reason || DEFAULT_NO_REASON,
|
|
745
|
+
git_user: `${userName.trim()} <${userEmail.trim()}>`,
|
|
746
|
+
git_commit: commitHash.trim(),
|
|
747
|
+
};
|
|
748
|
+
const line = JSON.stringify(entry);
|
|
749
|
+
appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
750
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-COS-gates event logged to ${auditPath}`);
|
|
751
|
+
}
|
|
752
|
+
// WU-2308: validateAllPreCommitHooks moved to wu-done-validators.mjs
|
|
753
|
+
// Now accepts worktreePath parameter to run audit from worktree context
|
|
754
|
+
/**
|
|
755
|
+
* Check if node_modules in worktree may be stale
|
|
756
|
+
* Detects when package.json differs between main and worktree, which indicates
|
|
757
|
+
* dependencies were added/removed but pnpm install may not have run in worktree.
|
|
758
|
+
* This prevents confusing typecheck failures due to missing dependencies.
|
|
759
|
+
* @param {string} worktreePath - Path to worktree
|
|
760
|
+
*/
|
|
761
|
+
function checkNodeModulesStaleness(worktreePath) {
|
|
762
|
+
try {
|
|
763
|
+
const mainPackageJson = path.resolve('package.json');
|
|
764
|
+
const worktreePackageJson = path.resolve(worktreePath, 'package.json');
|
|
765
|
+
if (!existsSync(mainPackageJson) || !existsSync(worktreePackageJson)) {
|
|
766
|
+
// No package.json to compare
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const mainContent = readFileSync(mainPackageJson, {
|
|
770
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
771
|
+
});
|
|
772
|
+
const worktreeContent = readFileSync(worktreePackageJson, {
|
|
773
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
774
|
+
});
|
|
775
|
+
// Compare package.json files
|
|
776
|
+
if (mainContent !== worktreeContent) {
|
|
777
|
+
const worktreeNodeModules = path.resolve(worktreePath, 'node_modules');
|
|
778
|
+
// Check if node_modules exists and when it was last modified
|
|
779
|
+
if (existsSync(worktreeNodeModules)) {
|
|
780
|
+
const nodeModulesStat = statSync(worktreeNodeModules);
|
|
781
|
+
const packageJsonStat = statSync(worktreePackageJson);
|
|
782
|
+
// If package.json is newer than node_modules, dependencies may be stale
|
|
783
|
+
if (packageJsonStat.mtimeMs > nodeModulesStat.mtimeMs) {
|
|
784
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: Potentially stale node_modules detected\n\n` +
|
|
785
|
+
` package.json in worktree differs from main checkout\n` +
|
|
786
|
+
` node_modules was last modified: ${nodeModulesStat.mtime.toISOString()}\n` +
|
|
787
|
+
` package.json was last modified: ${packageJsonStat.mtime.toISOString()}\n\n` +
|
|
788
|
+
` If gates fail with missing dependencies/types, run:\n` +
|
|
789
|
+
` cd ${worktreePath}\n` +
|
|
790
|
+
` pnpm install\n` +
|
|
791
|
+
` cd -\n` +
|
|
792
|
+
` pnpm wu:done --id <WU-ID>\n`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// node_modules doesn't exist at all
|
|
797
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: node_modules missing in worktree\n\n` +
|
|
798
|
+
` package.json in worktree differs from main checkout\n` +
|
|
799
|
+
` but node_modules directory does not exist\n\n` +
|
|
800
|
+
` If gates fail with missing dependencies/types, run:\n` +
|
|
801
|
+
` cd ${worktreePath}\n` +
|
|
802
|
+
` pnpm install\n` +
|
|
803
|
+
` cd -\n` +
|
|
804
|
+
` pnpm wu:done --id <WU-ID>\n`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch (e) {
|
|
809
|
+
// Non-critical check - just warn if it fails
|
|
810
|
+
console.warn(`${LOG_PREFIX.DONE} Could not check node_modules staleness: ${e.message}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function runGatesInWorktree(worktreePath, id, isDocsOnly = false) {
|
|
814
|
+
console.log(`\n${LOG_PREFIX.DONE} Running gates in worktree: ${worktreePath}`);
|
|
815
|
+
// Check for stale node_modules before running gates (prevents confusing failures)
|
|
816
|
+
checkNodeModulesStaleness(worktreePath);
|
|
817
|
+
const gatesCmd = isDocsOnly
|
|
818
|
+
? `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`
|
|
819
|
+
: `${PKG_MANAGER} ${SCRIPTS.GATES}`;
|
|
820
|
+
if (isDocsOnly) {
|
|
821
|
+
console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
|
|
822
|
+
}
|
|
823
|
+
const startTime = Date.now();
|
|
824
|
+
try {
|
|
825
|
+
// WU-1230: Pass WU_ID to validator for context-aware validation
|
|
826
|
+
execSync(gatesCmd, {
|
|
827
|
+
cwd: worktreePath,
|
|
828
|
+
stdio: 'inherit',
|
|
829
|
+
env: { ...process.env, WU_ID: id },
|
|
830
|
+
});
|
|
831
|
+
const duration = Date.now() - startTime;
|
|
832
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
|
|
833
|
+
emitTelemetry({ script: 'wu-done', wu_id: id, step: 'gates', ok: true, duration_ms: duration });
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
const duration = Date.now() - startTime;
|
|
838
|
+
emitTelemetry({
|
|
839
|
+
script: 'wu-done',
|
|
840
|
+
wu_id: id,
|
|
841
|
+
step: 'gates',
|
|
842
|
+
ok: false,
|
|
843
|
+
duration_ms: duration,
|
|
844
|
+
});
|
|
845
|
+
// WU-1280: Prominent error summary box (visible after ~130k chars of gate output)
|
|
846
|
+
// WU-1281: Extracted to helper using pretty-ms for duration formatting
|
|
847
|
+
printGateFailureBox({ id, location: worktreePath, durationMs: duration, isWorktreeMode: true });
|
|
848
|
+
die(`Gates failed in ${worktreePath}. Fix issues in the worktree and try again.`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function validateStagedFiles(id, isDocsOnly = false) {
|
|
852
|
+
const staged = await listStaged();
|
|
853
|
+
// WU-1740: Include wu-events.jsonl to persist state store events
|
|
854
|
+
const whitelist = [
|
|
855
|
+
`docs/04-operations/tasks/wu/${id}.yaml`,
|
|
856
|
+
'docs/04-operations/tasks/status.md',
|
|
857
|
+
'docs/04-operations/tasks/backlog.md',
|
|
858
|
+
WU_EVENTS_PATH,
|
|
859
|
+
];
|
|
860
|
+
if (isDocsOnly) {
|
|
861
|
+
// For docs-only WUs, validate that all staged files are in allowed paths
|
|
862
|
+
const docsResult = validateDocsOnly(staged);
|
|
863
|
+
if (!docsResult.valid) {
|
|
864
|
+
die(`Docs-only WU cannot modify code files:\n ${docsResult.violations.join(`${STRING_LITERALS.NEWLINE} `)}\n\n${getAllowedPathsDescription()}`);
|
|
865
|
+
}
|
|
866
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Docs-only path validation passed`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const unexpected = staged.filter((file) => {
|
|
870
|
+
// Whitelist exact matches
|
|
871
|
+
if (whitelist.includes(file))
|
|
872
|
+
return false;
|
|
873
|
+
// Whitelist .beacon/stamps/** pattern
|
|
874
|
+
if (file.startsWith('.beacon/stamps/'))
|
|
875
|
+
return false;
|
|
876
|
+
return true;
|
|
877
|
+
});
|
|
878
|
+
if (unexpected.length > 0) {
|
|
879
|
+
const otherWuYamlOnly = unexpected.every((f) => /^docs\/04-operations\/tasks\/wu\/WU-\d+\.yaml$/.test(f));
|
|
880
|
+
if (otherWuYamlOnly) {
|
|
881
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: other WU YAMLs are staged; proceeding and committing only current WU files.`);
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
die(`Unexpected files staged (only current WU YAML, status.md, backlog.md, .beacon/stamps/<id>.done allowed):\n ${unexpected.join(`${STRING_LITERALS.NEWLINE} `)}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Note: updateStatusRemoveInProgress, addToStatusCompleted, and moveWUToDoneBacklog
|
|
889
|
+
// have been extracted to tools/lib/wu-status-updater.mjs and imported above (WU-1163)
|
|
890
|
+
//
|
|
891
|
+
// Note: ensureStamp has been replaced with createStamp from tools/lib/stamp-utils.mjs (WU-1163)
|
|
892
|
+
//
|
|
893
|
+
// Note: readWUPreferWorktree, detectCurrentWorktree, defaultWorktreeFrom, detectWorkspaceMode,
|
|
894
|
+
// defaultBranchFrom, branchExists, runCleanup have been extracted to
|
|
895
|
+
// tools/lib/wu-done-validators.mjs and imported above (WU-1215)
|
|
896
|
+
/**
|
|
897
|
+
* Validate Branch-Only mode requirements before proceeding
|
|
898
|
+
* @param {string} laneBranch - Expected lane branch name
|
|
899
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
900
|
+
*/
|
|
901
|
+
async function validateBranchOnlyMode(laneBranch) {
|
|
902
|
+
// Check we're on the correct lane branch
|
|
903
|
+
const gitAdapter = getGitForCwd();
|
|
904
|
+
const currentBranch = await gitAdapter.getCurrentBranch();
|
|
905
|
+
if (currentBranch !== laneBranch) {
|
|
906
|
+
return {
|
|
907
|
+
valid: false,
|
|
908
|
+
error: `Branch-Only mode error: Not on the lane branch.\n\n` +
|
|
909
|
+
`Expected branch: ${laneBranch}\n` +
|
|
910
|
+
`Current branch: ${currentBranch}\n\n` +
|
|
911
|
+
`Fix: git checkout ${laneBranch}`,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
// Check working directory is clean
|
|
915
|
+
const status = await gitAdapter.getStatus();
|
|
916
|
+
if (status) {
|
|
917
|
+
return {
|
|
918
|
+
valid: false,
|
|
919
|
+
error: `Branch-Only mode error: Working directory is not clean.\n\n` +
|
|
920
|
+
`Uncommitted changes detected:\n${status}\n\n` +
|
|
921
|
+
`Fix: Commit all changes before running wu:done\n` +
|
|
922
|
+
` git add -A\n` +
|
|
923
|
+
` git commit -m "wu(wu-xxx): ..."\n` +
|
|
924
|
+
` git push origin ${laneBranch}`,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return { valid: true, error: null };
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* WU-755 + WU-1230: Record transaction state for rollback
|
|
931
|
+
* @param {string} id - WU ID
|
|
932
|
+
* @param {string} wuPath - Path to WU YAML
|
|
933
|
+
* @param {string} stampPath - Path to stamp file
|
|
934
|
+
* @param {string} backlogPath - Path to backlog.md (WU-1230)
|
|
935
|
+
* @param {string} statusPath - Path to status.md (WU-1230)
|
|
936
|
+
* @returns {object} - Transaction state for rollback
|
|
937
|
+
*/
|
|
938
|
+
function recordTransactionState(id, wuPath, stampPath, backlogPath, statusPath) {
|
|
939
|
+
const gitAdapter = getGitForCwd();
|
|
940
|
+
return {
|
|
941
|
+
id,
|
|
942
|
+
timestamp: new Date().toISOString(),
|
|
943
|
+
wuYamlContent: existsSync(wuPath)
|
|
944
|
+
? readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 })
|
|
945
|
+
: null,
|
|
946
|
+
stampExisted: existsSync(stampPath),
|
|
947
|
+
backlogContent: existsSync(backlogPath)
|
|
948
|
+
? readFileSync(backlogPath, { encoding: FILE_SYSTEM.UTF8 })
|
|
949
|
+
: null,
|
|
950
|
+
statusContent: existsSync(statusPath)
|
|
951
|
+
? readFileSync(statusPath, { encoding: FILE_SYSTEM.UTF8 })
|
|
952
|
+
: null,
|
|
953
|
+
mainSHA: gitAdapter.getCommitHash(),
|
|
954
|
+
laneBranch: gitAdapter.getCurrentBranch(),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* WU-755 + WU-1230: Rollback transaction on failure
|
|
959
|
+
* @param {object} txState - Transaction state from recordTransactionState
|
|
960
|
+
* @param {string} wuPath - Path to WU YAML
|
|
961
|
+
* @param {string} stampPath - Path to stamp file
|
|
962
|
+
* @param {string} backlogPath - Path to backlog.md (WU-1230)
|
|
963
|
+
* @param {string} statusPath - Path to status.md (WU-1230)
|
|
964
|
+
*/
|
|
965
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
966
|
+
async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
|
|
967
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
|
|
968
|
+
// WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
|
|
969
|
+
// Previous order (restore → git checkout) caused issues:
|
|
970
|
+
// - git checkout -- . would UNDO file restorations
|
|
971
|
+
// - Left messy state with staged + unstaged conflicts
|
|
972
|
+
//
|
|
973
|
+
// New order:
|
|
974
|
+
// 1. Unstage everything (git reset HEAD)
|
|
975
|
+
// 2. Discard working tree changes (git checkout -- .)
|
|
976
|
+
// 3. Remove stamp if created
|
|
977
|
+
// 4. THEN restore files from txState
|
|
978
|
+
// Step 1: Unstage any staged files FIRST
|
|
979
|
+
// Emergency fix Session 2: Use git-adapter instead of raw execSync
|
|
980
|
+
try {
|
|
981
|
+
const gitAdapter = getGitForCwd();
|
|
982
|
+
await gitAdapter.raw(['reset', 'HEAD']);
|
|
983
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Unstaged all files`);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// Ignore - may not have anything staged
|
|
987
|
+
}
|
|
988
|
+
// Step 2: Discard working directory changes (reset to last commit)
|
|
989
|
+
// Emergency fix Session 2: Use git-adapter instead of raw execSync
|
|
990
|
+
try {
|
|
991
|
+
const gitAdapter = getGitForCwd();
|
|
992
|
+
await gitAdapter.raw(['checkout', '--', '.']);
|
|
993
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset working tree to HEAD`);
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
// Ignore - may not have anything to discard
|
|
997
|
+
}
|
|
998
|
+
// Step 3: Remove stamp unconditionally if it exists (WU-1440)
|
|
999
|
+
// Previous behavior only removed if !stampExisted, but that flag could be wrong
|
|
1000
|
+
// due to edge cases. Unconditional removal ensures clean rollback state.
|
|
1001
|
+
if (existsSync(stampPath)) {
|
|
1002
|
+
try {
|
|
1003
|
+
unlinkSync(stampPath);
|
|
1004
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed ${stampPath}`);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to remove stamp: ${err.message}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Step 4: Restore files from txState (AFTER git cleanup)
|
|
1011
|
+
// Build list of files to restore with per-file error tracking (ref: WU-1255)
|
|
1012
|
+
const filesToRestore = [];
|
|
1013
|
+
// Restore backlog.md (ref: WU-1230)
|
|
1014
|
+
if (txState.backlogContent && existsSync(backlogPath)) {
|
|
1015
|
+
filesToRestore.push({ name: 'backlog.md', path: backlogPath, content: txState.backlogContent });
|
|
1016
|
+
}
|
|
1017
|
+
// Restore status.md (ref: WU-1230)
|
|
1018
|
+
if (txState.statusContent && existsSync(statusPath)) {
|
|
1019
|
+
filesToRestore.push({ name: 'status.md', path: statusPath, content: txState.statusContent });
|
|
1020
|
+
}
|
|
1021
|
+
// Restore WU YAML if it was modified
|
|
1022
|
+
if (txState.wuYamlContent && existsSync(wuPath)) {
|
|
1023
|
+
filesToRestore.push({ name: 'WU YAML', path: wuPath, content: txState.wuYamlContent });
|
|
1024
|
+
}
|
|
1025
|
+
// WU-1255: Use rollbackFiles utility for per-file error tracking
|
|
1026
|
+
const restoreResult = rollbackFiles(filesToRestore);
|
|
1027
|
+
// Log results
|
|
1028
|
+
for (const name of restoreResult.restored) {
|
|
1029
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Restored ${name}`);
|
|
1030
|
+
}
|
|
1031
|
+
for (const err of restoreResult.errors) {
|
|
1032
|
+
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to restore ${err.name}: ${err.error}`);
|
|
1033
|
+
}
|
|
1034
|
+
// Reset main to original SHA if we're on main
|
|
1035
|
+
try {
|
|
1036
|
+
const gitAdapter = getGitForCwd();
|
|
1037
|
+
const currentBranch = await gitAdapter.getCurrentBranch();
|
|
1038
|
+
if (currentBranch === BRANCHES.MAIN) {
|
|
1039
|
+
const currentSHA = await gitAdapter.getCommitHash();
|
|
1040
|
+
if (currentSHA !== txState.mainSHA) {
|
|
1041
|
+
await gitAdapter.reset(txState.mainSHA, { hard: true });
|
|
1042
|
+
// Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
|
|
1043
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset main to ${txState.mainSHA.slice(0, GIT.SHA_SHORT_LENGTH)}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
catch (e) {
|
|
1048
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not reset main: ${e.message}`);
|
|
1049
|
+
}
|
|
1050
|
+
// WU-1280: Verify clean git status after rollback
|
|
1051
|
+
// WU-1281: Extracted to helper to fix repeated parsing and magic number
|
|
1052
|
+
// Emergency fix Session 2: Use git-adapter instead of raw execSync
|
|
1053
|
+
try {
|
|
1054
|
+
const gitAdapter = getGitForCwd();
|
|
1055
|
+
const statusOutput = (await gitAdapter.raw(['status', '--porcelain'])).trim();
|
|
1056
|
+
if (statusOutput) {
|
|
1057
|
+
printStatusPreview(statusOutput);
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
console.log(`${LOG_PREFIX.DONE} ✅ Working tree is clean`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
// Ignore - git status may fail in edge cases
|
|
1065
|
+
}
|
|
1066
|
+
// WU-1255: Report final status with all errors
|
|
1067
|
+
if (restoreResult.errors.length > 0) {
|
|
1068
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback completed with ${restoreResult.errors.length} error(s):`);
|
|
1069
|
+
for (const err of restoreResult.errors) {
|
|
1070
|
+
console.error(` - ${err.name}: ${err.error}`);
|
|
1071
|
+
}
|
|
1072
|
+
console.error(`${LOG_PREFIX.DONE} Manual intervention required for failed files`);
|
|
1073
|
+
console.error(`${LOG_PREFIX.DONE} See playbook.md section 12 "Scenario D" for recovery steps`);
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Rollback complete - WU state fully reverted (no infinite loop)`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Validate WU code paths for incomplete work markers and Mock classes
|
|
1081
|
+
* @param {object} doc - WU YAML document
|
|
1082
|
+
* @param {string} id - WU ID
|
|
1083
|
+
* @param {boolean} allowTodo - Allow incomplete work markers (with warning)
|
|
1084
|
+
* @param {string|null} worktreePath - Path to worktree to validate files from
|
|
1085
|
+
*/
|
|
1086
|
+
function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
|
|
1087
|
+
console.log(`\n${LOG_PREFIX.DONE} Running WU validator for ${id}...`);
|
|
1088
|
+
// Check if WU has code_paths defined
|
|
1089
|
+
const codePaths = doc.code_paths || [];
|
|
1090
|
+
if (codePaths.length === 0) {
|
|
1091
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} No code_paths defined in WU YAML, skipping validator`);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
// Check if incomplete work flag requires justification in notes
|
|
1095
|
+
if (allowTodo) {
|
|
1096
|
+
// Handle both string and array formats for notes (WU-654)
|
|
1097
|
+
let notesText = '';
|
|
1098
|
+
if (typeof doc.notes === 'string') {
|
|
1099
|
+
notesText = doc.notes;
|
|
1100
|
+
}
|
|
1101
|
+
else if (Array.isArray(doc.notes)) {
|
|
1102
|
+
notesText = doc.notes.join(STRING_LITERALS.NEWLINE);
|
|
1103
|
+
}
|
|
1104
|
+
const hasJustification = notesText.toLowerCase().includes('todo') || notesText.toLowerCase().includes('allow-todo');
|
|
1105
|
+
if (!hasJustification) {
|
|
1106
|
+
die('--allow-todo flag requires justification in WU YAML notes field.\n' +
|
|
1107
|
+
'Add a note explaining why TODOs are acceptable for this WU.');
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Validate from worktree if available (ensures we check the lane branch code)
|
|
1111
|
+
const validateOptions = { allowTodos: allowTodo };
|
|
1112
|
+
if (worktreePath && existsSync(worktreePath)) {
|
|
1113
|
+
validateOptions.worktreePath = worktreePath;
|
|
1114
|
+
console.log(`${LOG_PREFIX.DONE} Validating code paths from worktree: ${worktreePath}`);
|
|
1115
|
+
}
|
|
1116
|
+
// Run validation
|
|
1117
|
+
const result = validateWUCodePaths(codePaths, validateOptions);
|
|
1118
|
+
// Display warnings
|
|
1119
|
+
if (result.warnings.length > 0) {
|
|
1120
|
+
console.log('\n⚠️ WU VALIDATOR WARNINGS:');
|
|
1121
|
+
result.warnings.forEach((warning) => console.log(warning));
|
|
1122
|
+
}
|
|
1123
|
+
// Handle errors
|
|
1124
|
+
if (!result.valid) {
|
|
1125
|
+
console.log('\n❌ WU VALIDATOR FAILED:');
|
|
1126
|
+
result.errors.forEach((error) => console.log(error));
|
|
1127
|
+
console.log('\nFix these issues before marking WU as done.');
|
|
1128
|
+
console.log('Alternatively, use --allow-todo if TODOs are acceptable (requires justification in notes).');
|
|
1129
|
+
die('WU validation failed. See errors above.');
|
|
1130
|
+
}
|
|
1131
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU validator passed`);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* GUARDRAIL 2: Enforce ownership semantics in wu:done
|
|
1135
|
+
*
|
|
1136
|
+
* Validates that the current user owns the WU before allowing completion.
|
|
1137
|
+
* Prevents agents/humans from finishing WUs they do not own.
|
|
1138
|
+
*
|
|
1139
|
+
* @param {string} id - WU ID
|
|
1140
|
+
* @param {object} doc - WU YAML document
|
|
1141
|
+
* @param {string|null} worktreePath - Expected worktree path
|
|
1142
|
+
* @param {boolean} overrideOwner - Override flag (requires reason)
|
|
1143
|
+
* @param {string|null} overrideReason - Reason for override
|
|
1144
|
+
* @returns {{valid: boolean, error: string|null, auditEntry: object|null}}
|
|
1145
|
+
*/
|
|
1146
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
1147
|
+
async function checkOwnership(id, doc, worktreePath, overrideOwner = false, overrideReason = null) {
|
|
1148
|
+
// Missing worktree means WU was not claimed properly (unless escape hatch applies)
|
|
1149
|
+
if (!worktreePath || !existsSync(worktreePath)) {
|
|
1150
|
+
return {
|
|
1151
|
+
valid: false,
|
|
1152
|
+
error: `Missing worktree for ${id}.\n\n` +
|
|
1153
|
+
`Expected worktree at: ${worktreePath || 'unknown'}\n\n` +
|
|
1154
|
+
`Worktrees are required for proper WU completion in Worktree mode.\n` +
|
|
1155
|
+
`If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`,
|
|
1156
|
+
auditEntry: null,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
// Get assigned owner from WU YAML - read directly from worktree to ensure we get the lane branch version
|
|
1160
|
+
let assignedTo = doc.assigned_to || null;
|
|
1161
|
+
if (!assignedTo && worktreePath) {
|
|
1162
|
+
// Fallback: Read directly from worktree YAML if not present in doc (fixes WU-1106)
|
|
1163
|
+
const wtWUPath = path.join(worktreePath, WU_PATHS.WU(id));
|
|
1164
|
+
if (existsSync(wtWUPath)) {
|
|
1165
|
+
try {
|
|
1166
|
+
const text = readFileSync(wtWUPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
1167
|
+
const wtDoc = parseYAML(text);
|
|
1168
|
+
assignedTo = wtDoc?.assigned_to || null;
|
|
1169
|
+
if (assignedTo) {
|
|
1170
|
+
console.log(`${LOG_PREFIX.DONE} Note: Read assigned_to from worktree YAML (not found in main)`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Failed to read assigned_to from worktree: ${err.message}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (!assignedTo) {
|
|
1179
|
+
return {
|
|
1180
|
+
valid: false,
|
|
1181
|
+
error: `WU ${id} has no assigned_to field.\n\n` +
|
|
1182
|
+
`This WU was claimed before ownership tracking was implemented.\n` +
|
|
1183
|
+
`To complete this WU:\n` +
|
|
1184
|
+
` 1. Add assigned_to: <your-email> to ${id}.yaml\n` +
|
|
1185
|
+
` 2. Commit the change\n` +
|
|
1186
|
+
` 3. Re-run: pnpm wu:done --id ${id}`,
|
|
1187
|
+
auditEntry: null,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
// Get current user identity
|
|
1191
|
+
let currentUser = null;
|
|
1192
|
+
try {
|
|
1193
|
+
currentUser = (await getGitForCwd().getConfigValue(GIT_CONFIG_USER_EMAIL)).trim();
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
// Fallback to environment variable
|
|
1197
|
+
currentUser = process.env.GIT_USER || process.env.USER || null;
|
|
1198
|
+
}
|
|
1199
|
+
if (!currentUser) {
|
|
1200
|
+
return {
|
|
1201
|
+
valid: false,
|
|
1202
|
+
error: `Cannot determine current user identity.\n\n` +
|
|
1203
|
+
`Set git user.email or GIT_USER environment variable.`,
|
|
1204
|
+
auditEntry: null,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
// WU-1234: Normalize usernames for comparison (allows email vs username match)
|
|
1208
|
+
// e.g., tom@hellm.ai matches 'tom' assigned_to field
|
|
1209
|
+
const normalizedAssigned = normalizeUsername(assignedTo);
|
|
1210
|
+
const normalizedCurrent = normalizeUsername(currentUser);
|
|
1211
|
+
const isOwner = normalizedAssigned === normalizedCurrent;
|
|
1212
|
+
if (isOwner) {
|
|
1213
|
+
// Owner is completing their own WU - allow
|
|
1214
|
+
if (assignedTo !== currentUser) {
|
|
1215
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Ownership match via normalization: "${assignedTo}" == "${currentUser}"`);
|
|
1216
|
+
}
|
|
1217
|
+
return { valid: true, error: null, auditEntry: null };
|
|
1218
|
+
}
|
|
1219
|
+
// Not the owner - check for override
|
|
1220
|
+
if (overrideOwner) {
|
|
1221
|
+
if (!overrideReason) {
|
|
1222
|
+
return {
|
|
1223
|
+
valid: false,
|
|
1224
|
+
error: `--override-owner requires --reason "<why you're completing someone else's WU>"`,
|
|
1225
|
+
auditEntry: null,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
// Create audit entry
|
|
1229
|
+
const auditEntry = {
|
|
1230
|
+
timestamp: new Date().toISOString(),
|
|
1231
|
+
wu_id: id,
|
|
1232
|
+
assigned_to: assignedTo,
|
|
1233
|
+
completed_by: currentUser,
|
|
1234
|
+
reason: overrideReason,
|
|
1235
|
+
git_commit: (await getGitForCwd().getCommitHash()).trim(),
|
|
1236
|
+
};
|
|
1237
|
+
console.log(`\n⚠️ --override-owner: Completing WU assigned to someone else`);
|
|
1238
|
+
console.log(` Assigned to: ${assignedTo}`);
|
|
1239
|
+
console.log(` Completed by: ${currentUser}`);
|
|
1240
|
+
console.log(` Reason: ${overrideReason}\n`);
|
|
1241
|
+
return { valid: true, error: null, auditEntry };
|
|
1242
|
+
}
|
|
1243
|
+
// Not the owner and no override - block
|
|
1244
|
+
return {
|
|
1245
|
+
valid: false,
|
|
1246
|
+
error: `\n❌ OWNERSHIP VIOLATION: ${id} is assigned to someone else\n\n` +
|
|
1247
|
+
` Assigned to: ${assignedTo}\n` +
|
|
1248
|
+
` Current user: ${currentUser}\n\n` +
|
|
1249
|
+
` You cannot complete WUs you do not own.\n\n` +
|
|
1250
|
+
` 📋 Options:\n` +
|
|
1251
|
+
` 1. Contact ${assignedTo} to complete the WU\n` +
|
|
1252
|
+
` 2. Reassign the WU to yourself in ${id}.yaml (requires approval)\n` +
|
|
1253
|
+
` 3. Add co_assigned field for pairing (requires approval)\n\n` +
|
|
1254
|
+
` ⚠️ To override (use with extreme caution):\n` +
|
|
1255
|
+
` pnpm wu:done --id ${id} --override-owner --reason "<why>"\n\n` +
|
|
1256
|
+
` AGENTS: NEVER use --override-owner without explicit instruction.\n` +
|
|
1257
|
+
` Language protocol: "pick up WU-${id.replace('WU-', '')}" = READ ONLY.\n`,
|
|
1258
|
+
auditEntry: null,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Log ownership override to audit trail
|
|
1263
|
+
* @param {object} auditEntry - Audit entry to log
|
|
1264
|
+
*/
|
|
1265
|
+
function auditOwnershipOverride(auditEntry) {
|
|
1266
|
+
const auditPath = path.join('.beacon', 'ownership-override-audit.log');
|
|
1267
|
+
const auditDir = path.dirname(auditPath);
|
|
1268
|
+
if (!existsSync(auditDir))
|
|
1269
|
+
mkdirSync(auditDir, { recursive: true });
|
|
1270
|
+
const line = JSON.stringify(auditEntry);
|
|
1271
|
+
appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
|
|
1272
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Ownership override logged to ${auditPath}`);
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Execute pre-flight checks before gates
|
|
1276
|
+
* Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #3)
|
|
1277
|
+
* @param {object} params - Parameters
|
|
1278
|
+
* @param {string} params.id - WU ID
|
|
1279
|
+
* @param {object} params.args - Parsed CLI arguments
|
|
1280
|
+
* @param {boolean} params.isBranchOnly - Whether in branch-only mode
|
|
1281
|
+
* @param {boolean} params.isDocsOnly - Whether this is a docs-only WU
|
|
1282
|
+
* @param {object} params.docMain - Main WU YAML document
|
|
1283
|
+
* @param {object} params.docForValidation - WU YAML document to validate (worktree or main)
|
|
1284
|
+
* @param {string|null} params.derivedWorktree - Derived worktree path
|
|
1285
|
+
* @returns {Promise<{title: string, docForValidation: object}>} Updated title and doc
|
|
1286
|
+
*/
|
|
1287
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
1288
|
+
async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docMain, docForValidation, derivedWorktree, }) {
|
|
1289
|
+
// YAML schema validation
|
|
1290
|
+
console.log(`${LOG_PREFIX.DONE} Validating WU YAML structure...`);
|
|
1291
|
+
const schemaResult = validateWU(docForValidation);
|
|
1292
|
+
if (!schemaResult.success) {
|
|
1293
|
+
const errors = schemaResult.error.issues
|
|
1294
|
+
.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)
|
|
1295
|
+
.join(STRING_LITERALS.NEWLINE);
|
|
1296
|
+
die(`❌ WU YAML validation failed:\n\n${errors}\n\nFix these issues before running wu:done`);
|
|
1297
|
+
}
|
|
1298
|
+
// Additional done-specific validation
|
|
1299
|
+
if (docForValidation.status === WU_STATUS.DONE) {
|
|
1300
|
+
const doneResult = validateDoneWU(schemaResult.data);
|
|
1301
|
+
if (!doneResult.valid) {
|
|
1302
|
+
die(`❌ WU not ready for done status:\n\n${doneResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML validation passed`);
|
|
1306
|
+
// WU-2079: Approval gate validation
|
|
1307
|
+
// Ensures required approvals are present before allowing completion
|
|
1308
|
+
console.log(`${LOG_PREFIX.DONE} Checking approval gates...`);
|
|
1309
|
+
const approvalResult = validateApprovalGates(schemaResult.data);
|
|
1310
|
+
if (!approvalResult.valid) {
|
|
1311
|
+
die(`❌ Approval gates not satisfied:\n\n${approvalResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}\n\n` +
|
|
1312
|
+
`📋 To fix:\n` +
|
|
1313
|
+
` 1. Request approval from the required role(s)\n` +
|
|
1314
|
+
` 2. Add their email(s) to the 'approved_by' field in the WU YAML\n` +
|
|
1315
|
+
` 3. Re-run: pnpm wu:done --id ${id}\n\n` +
|
|
1316
|
+
` See docs/04-operations/governance/project-governance.md for role definitions.`);
|
|
1317
|
+
}
|
|
1318
|
+
// Log advisory warnings (non-blocking)
|
|
1319
|
+
if (approvalResult.warnings.length > 0) {
|
|
1320
|
+
approvalResult.warnings.forEach((w) => {
|
|
1321
|
+
console.warn(`${LOG_PREFIX.DONE} ⚠️ ${w}`);
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Approval gates passed`);
|
|
1325
|
+
// WU-1805: Preflight code_paths and test_paths validation
|
|
1326
|
+
// Run BEFORE gates to catch YAML mismatches early (saves time vs. discovering after full gate run)
|
|
1327
|
+
const preflightResult = await executePreflightCodePathValidation(id, {
|
|
1328
|
+
rootDir: process.cwd(),
|
|
1329
|
+
worktreePath: derivedWorktree,
|
|
1330
|
+
});
|
|
1331
|
+
if (!preflightResult.valid) {
|
|
1332
|
+
const errorMessage = buildPreflightCodePathErrorMessage(id, preflightResult);
|
|
1333
|
+
die(errorMessage);
|
|
1334
|
+
}
|
|
1335
|
+
// WU-2310: Preflight type vs code_paths validation
|
|
1336
|
+
// Run BEFORE transaction to prevent documentation WUs with code paths from failing at git commit
|
|
1337
|
+
console.log(`${LOG_PREFIX.DONE} Validating type vs code_paths (WU-2310)...`);
|
|
1338
|
+
const typeVsCodePathsResult = validateTypeVsCodePathsPreflight(docForValidation);
|
|
1339
|
+
if (!typeVsCodePathsResult.valid) {
|
|
1340
|
+
const errorMessage = buildTypeVsCodePathsErrorMessage(id, typeVsCodePathsResult.blockedPaths);
|
|
1341
|
+
die(errorMessage);
|
|
1342
|
+
}
|
|
1343
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Type vs code_paths validation passed`);
|
|
1344
|
+
// Tripwire: Scan commands log for violations
|
|
1345
|
+
runTripwireCheck();
|
|
1346
|
+
// WU-1234: Pre-flight backlog consistency check
|
|
1347
|
+
// Fail fast if WU is in both Done and In Progress sections
|
|
1348
|
+
console.log(`${LOG_PREFIX.DONE} Checking backlog consistency...`);
|
|
1349
|
+
const backlogPath = WU_PATHS.BACKLOG();
|
|
1350
|
+
const backlogConsistency = checkBacklogConsistencyForWU(id, backlogPath);
|
|
1351
|
+
if (!backlogConsistency.valid) {
|
|
1352
|
+
die(backlogConsistency.error);
|
|
1353
|
+
}
|
|
1354
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Backlog consistency check passed`);
|
|
1355
|
+
// WU-1276: Pre-flight WU state consistency check
|
|
1356
|
+
// Layer 2 defense-in-depth: fail fast if WU has pre-existing inconsistencies
|
|
1357
|
+
console.log(`${LOG_PREFIX.DONE} Checking WU state consistency...`);
|
|
1358
|
+
const stateCheck = await checkWUConsistency(id);
|
|
1359
|
+
if (!stateCheck.valid) {
|
|
1360
|
+
const errors = stateCheck.errors
|
|
1361
|
+
.map((e) => ` - ${e.type}: ${e.description}`)
|
|
1362
|
+
.join(STRING_LITERALS.NEWLINE);
|
|
1363
|
+
die(`Pre-existing inconsistencies for ${id}:\n${errors}\n\n` +
|
|
1364
|
+
`Fix with: pnpm wu:repair --id ${id}`);
|
|
1365
|
+
}
|
|
1366
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU state consistency check passed`);
|
|
1367
|
+
// Branch-Only mode validation
|
|
1368
|
+
if (isBranchOnly) {
|
|
1369
|
+
const laneBranch = await defaultBranchFrom(docMain);
|
|
1370
|
+
if (!laneBranch)
|
|
1371
|
+
die('Cannot determine lane branch from WU YAML');
|
|
1372
|
+
const validation = await validateBranchOnlyMode(laneBranch);
|
|
1373
|
+
if (!validation.valid) {
|
|
1374
|
+
die(validation.error);
|
|
1375
|
+
}
|
|
1376
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Branch-Only mode validation passed`);
|
|
1377
|
+
console.log(`${LOG_PREFIX.DONE} Working on branch: ${laneBranch}`);
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
// Worktree mode: must be on main
|
|
1381
|
+
await ensureOnMain(getGitForCwd());
|
|
1382
|
+
// Prevent multi-agent data loss by ensuring clean working tree
|
|
1383
|
+
await ensureCleanWorkingTree();
|
|
1384
|
+
// Prevent coordination failures by ensuring main is up-to-date
|
|
1385
|
+
await ensureMainUpToDate();
|
|
1386
|
+
// P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
|
|
1387
|
+
// Previous wu:done runs or memory layer writes may have left this file dirty,
|
|
1388
|
+
// which causes the auto-rebase to fail with "You have unstaged changes"
|
|
1389
|
+
if (derivedWorktree) {
|
|
1390
|
+
try {
|
|
1391
|
+
execSync(`git -C "${derivedWorktree}" restore "${WU_EVENTS_PATH}"`);
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
// Non-fatal: file might not exist or already clean
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
// WU-1382: Detect parallel completions and warn
|
|
1398
|
+
// WU-1584 Fix #3: Trigger auto-rebase instead of just warning
|
|
1399
|
+
console.log(`${LOG_PREFIX.DONE} Checking for parallel WU completions...`);
|
|
1400
|
+
const parallelResult = await detectParallelCompletions(id, docForValidation);
|
|
1401
|
+
if (parallelResult.hasParallelCompletions) {
|
|
1402
|
+
console.warn(parallelResult.warning);
|
|
1403
|
+
// Emit telemetry for parallel detection
|
|
1404
|
+
emitTelemetry({
|
|
1405
|
+
script: 'wu-done',
|
|
1406
|
+
wu_id: id,
|
|
1407
|
+
step: 'parallel_detection',
|
|
1408
|
+
parallel_wus: parallelResult.completedWUs,
|
|
1409
|
+
count: parallelResult.completedWUs.length,
|
|
1410
|
+
});
|
|
1411
|
+
// WU-1588: Check inbox for recent signals from parallel agents
|
|
1412
|
+
// Non-blocking: failures handled internally by checkInboxForRecentSignals
|
|
1413
|
+
await checkInboxForRecentSignals(id);
|
|
1414
|
+
// WU-1584: Instead of proceeding with warning, trigger auto-rebase
|
|
1415
|
+
// This prevents merge conflicts that would fail downstream
|
|
1416
|
+
if (derivedWorktree && !args.noAutoRebase) {
|
|
1417
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
|
|
1418
|
+
const laneBranch = await defaultBranchFrom(docForValidation);
|
|
1419
|
+
if (laneBranch) {
|
|
1420
|
+
const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
|
|
1421
|
+
if (rebaseResult.success) {
|
|
1422
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
|
|
1423
|
+
emitTelemetry({
|
|
1424
|
+
script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
|
|
1425
|
+
wu_id: id,
|
|
1426
|
+
step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
|
|
1427
|
+
parallel_wus: parallelResult.completedWUs,
|
|
1428
|
+
count: parallelResult.completedWUs.length,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
// Rebase failed - provide detailed instructions
|
|
1433
|
+
console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
|
|
1434
|
+
console.error(rebaseResult.error);
|
|
1435
|
+
die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
|
|
1436
|
+
`Manual resolution required - see instructions above.`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
else if (!args.noAutoRebase) {
|
|
1441
|
+
// No worktree path available - warn and proceed (legacy behavior)
|
|
1442
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
// Auto-rebase disabled - warn and proceed
|
|
1446
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase disabled (--no-auto-rebase) - proceeding with caution`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
// WU-1381: Detect background processes that might interfere with gates
|
|
1450
|
+
// Non-blocking warning - helps agents understand mixed stdout/stderr output
|
|
1451
|
+
if (derivedWorktree) {
|
|
1452
|
+
await runBackgroundProcessCheck(derivedWorktree);
|
|
1453
|
+
}
|
|
1454
|
+
// WU-1804: Fail fast before gates with comprehensive claim metadata check.
|
|
1455
|
+
// Validates both YAML status AND state store BEFORE gates, not just one of them.
|
|
1456
|
+
// Provides actionable guidance to run wu:repair-claim if validation fails.
|
|
1457
|
+
if (derivedWorktree) {
|
|
1458
|
+
await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// Use worktree title for commit message (not stale main title)
|
|
1462
|
+
const title = docForValidation.title || docMain.title || '';
|
|
1463
|
+
if (isDocsOnly) {
|
|
1464
|
+
console.log('\n📝 Docs-only WU detected');
|
|
1465
|
+
console.log(' - Gates will skip lint/typecheck/tests');
|
|
1466
|
+
console.log(' - Only docs/markdown paths allowed\n');
|
|
1467
|
+
}
|
|
1468
|
+
if (isBranchOnly) {
|
|
1469
|
+
console.log('\n🌿 Branch-Only mode detected');
|
|
1470
|
+
console.log(' - Gates run in main checkout on lane branch');
|
|
1471
|
+
console.log(' - No worktree to remove\n');
|
|
1472
|
+
}
|
|
1473
|
+
// Ownership check (skip in branch-only mode)
|
|
1474
|
+
if (!isBranchOnly) {
|
|
1475
|
+
const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
|
|
1476
|
+
if (!ownershipCheck.valid) {
|
|
1477
|
+
die(ownershipCheck.error);
|
|
1478
|
+
}
|
|
1479
|
+
// If override was used, log to audit trail and add to WU notes
|
|
1480
|
+
if (ownershipCheck.auditEntry) {
|
|
1481
|
+
auditOwnershipOverride(ownershipCheck.auditEntry);
|
|
1482
|
+
// Add override reason to WU notes (schema requires string, not array)
|
|
1483
|
+
const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
|
|
1484
|
+
appendNote(docForValidation, overrideNote);
|
|
1485
|
+
// Write updated WU YAML back to worktree
|
|
1486
|
+
if (derivedWorktree) {
|
|
1487
|
+
const wtWUPath = path.join(derivedWorktree, 'docs', '04-operations', 'tasks', 'wu', `${id}.yaml`);
|
|
1488
|
+
if (existsSync(wtWUPath)) {
|
|
1489
|
+
writeWU(wtWUPath, docForValidation);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
// WU-1280: Early spec completeness validation (before gates)
|
|
1495
|
+
// Catches missing tests.manual, empty code_paths, etc. BEFORE 2min gate run
|
|
1496
|
+
console.log(`\n${LOG_PREFIX.DONE} Validating spec completeness for ${id}...`);
|
|
1497
|
+
const specResult = validateSpecCompleteness(docForValidation, id);
|
|
1498
|
+
if (!specResult.valid) {
|
|
1499
|
+
console.error(`\n❌ Spec completeness validation failed for ${id}:\n`);
|
|
1500
|
+
specResult.errors.forEach((err) => console.error(` - ${err}`));
|
|
1501
|
+
console.error(`\nFix these issues before running wu:done:\n` +
|
|
1502
|
+
` 1. Update docs/04-operations/tasks/wu/${id}.yaml\n` +
|
|
1503
|
+
` 2. Fill description with Context/Problem/Solution\n` +
|
|
1504
|
+
` 3. Replace ${PLACEHOLDER_SENTINEL} text with specific criteria\n` +
|
|
1505
|
+
` 4. List all modified files in code_paths\n` +
|
|
1506
|
+
` 5. Add at least one test path (unit, e2e, integration, or manual)\n` +
|
|
1507
|
+
` 6. Re-run: pnpm wu:done --id ${id}\n\n` +
|
|
1508
|
+
`See: CLAUDE.md §2.7 "WUs are specs, not code"\n`);
|
|
1509
|
+
die(`Cannot mark ${id} as done - spec incomplete`);
|
|
1510
|
+
}
|
|
1511
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spec completeness check passed`);
|
|
1512
|
+
// WU-1351: Validate code_paths files exist (prevents false completions)
|
|
1513
|
+
// In worktree mode: validate files exist in worktree (will be merged)
|
|
1514
|
+
// In branch-only mode: validate files exist on current branch
|
|
1515
|
+
console.log(`\n${LOG_PREFIX.DONE} Validating code_paths existence for ${id}...`);
|
|
1516
|
+
const codePathsResult = await validateCodePathsExist(docForValidation, id, {
|
|
1517
|
+
worktreePath: derivedWorktree,
|
|
1518
|
+
targetBranch: isBranchOnly ? 'HEAD' : BRANCHES.MAIN,
|
|
1519
|
+
});
|
|
1520
|
+
if ('valid' in codePathsResult && !codePathsResult.valid) {
|
|
1521
|
+
console.error(`\n❌ code_paths validation failed for ${id}:\n`);
|
|
1522
|
+
if ('errors' in codePathsResult) {
|
|
1523
|
+
codePathsResult.errors.forEach((err) => console.error(err));
|
|
1524
|
+
}
|
|
1525
|
+
die(`Cannot mark ${id} as done - code_paths missing from target branch`);
|
|
1526
|
+
}
|
|
1527
|
+
// WU-1324 + WU-1542: Check mandatory agent compliance
|
|
1528
|
+
// WU-1542: --require-agents makes this a BLOCKING check
|
|
1529
|
+
const codePaths = docForValidation.code_paths || [];
|
|
1530
|
+
const compliance = checkMandatoryAgentsComplianceBlocking(codePaths, id, {
|
|
1531
|
+
blocking: Boolean(args.requireAgents),
|
|
1532
|
+
});
|
|
1533
|
+
if (compliance.blocking && compliance.errorMessage) {
|
|
1534
|
+
// WU-1542: Blocking mode - fail wu:done with detailed error
|
|
1535
|
+
die(compliance.errorMessage);
|
|
1536
|
+
}
|
|
1537
|
+
else if (!compliance.compliant) {
|
|
1538
|
+
// Non-blocking mode - show warning (original WU-1324 behavior)
|
|
1539
|
+
console.warn(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} MANDATORY AGENT WARNING`);
|
|
1540
|
+
console.warn(`The following mandatory agents were not confirmed as invoked:`);
|
|
1541
|
+
for (const agent of compliance.missing) {
|
|
1542
|
+
console.warn(` • ${agent}`);
|
|
1543
|
+
}
|
|
1544
|
+
console.warn(`\nThis is a NON-BLOCKING warning.`);
|
|
1545
|
+
console.warn(`Use --require-agents to make this a blocking error.\n`);
|
|
1546
|
+
}
|
|
1547
|
+
// WU-1999: Exposure validation (NON-BLOCKING warning)
|
|
1548
|
+
printExposureWarnings(docForValidation, { skipExposureCheck: args.skipExposureCheck });
|
|
1549
|
+
// WU-2022: Feature accessibility validation (BLOCKING)
|
|
1550
|
+
validateAccessibilityOrDie(docForValidation, {
|
|
1551
|
+
skipAccessibilityCheck: args.skipAccessibilityCheck,
|
|
1552
|
+
});
|
|
1553
|
+
// Run WU validator
|
|
1554
|
+
runWUValidator(docForValidation, id, args.allowTodo, derivedWorktree);
|
|
1555
|
+
// Validate skip-gates requirements
|
|
1556
|
+
if (args.skipGates) {
|
|
1557
|
+
if (!args.reason) {
|
|
1558
|
+
die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
|
|
1559
|
+
}
|
|
1560
|
+
if (!args.fixWu) {
|
|
1561
|
+
die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
|
|
1562
|
+
}
|
|
1563
|
+
if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
|
|
1564
|
+
die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return { title, docForValidation };
|
|
1568
|
+
}
|
|
1569
|
+
async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath, branchName, }) {
|
|
1570
|
+
// WU-1747: Check if gates can be skipped based on valid checkpoint
|
|
1571
|
+
// This allows resuming wu:done without re-running gates if nothing changed
|
|
1572
|
+
const skipResult = canSkipGates(id, { currentHeadSha: undefined });
|
|
1573
|
+
if (skipResult.canSkip) {
|
|
1574
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${CHECKPOINT_MESSAGES.SKIPPING_GATES_VALID}`);
|
|
1575
|
+
console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.CHECKPOINT_LABEL}: ${skipResult.checkpoint.checkpointId}`);
|
|
1576
|
+
console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.GATES_PASSED_AT}: ${skipResult.checkpoint.gatesPassedAt}`);
|
|
1577
|
+
emitTelemetry({
|
|
1578
|
+
script: TELEMETRY_STEPS.GATES,
|
|
1579
|
+
wu_id: id,
|
|
1580
|
+
step: TELEMETRY_STEPS.GATES,
|
|
1581
|
+
skipped: true,
|
|
1582
|
+
reason: SKIP_GATES_REASONS.CHECKPOINT_VALID,
|
|
1583
|
+
checkpoint_id: skipResult.checkpoint.checkpointId,
|
|
1584
|
+
});
|
|
1585
|
+
return; // Skip gates entirely
|
|
1586
|
+
}
|
|
1587
|
+
// WU-1747: Create checkpoint before gates for resumption on failure
|
|
1588
|
+
if (worktreePath && branchName) {
|
|
1589
|
+
try {
|
|
1590
|
+
await createWU1747Checkpoint({ wuId: id, worktreePath, branchName }, { gatesPassed: false });
|
|
1591
|
+
}
|
|
1592
|
+
catch (err) {
|
|
1593
|
+
// Non-blocking: checkpoint failure should not block wu:done
|
|
1594
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} ${CHECKPOINT_MESSAGES.COULD_NOT_CREATE}: ${err.message}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// WU-1588: Create pre-gates checkpoint for recovery if gates fail
|
|
1598
|
+
// Non-blocking: failures handled internally by createPreGatesCheckpoint
|
|
1599
|
+
// WU-1749 Bug 5: Pass worktreePath as baseDir to write to worktree's wu-events.jsonl, not main's
|
|
1600
|
+
await createPreGatesCheckpoint(id, worktreePath, worktreePath);
|
|
1601
|
+
// P0 EMERGENCY FIX: Restore wu-events.jsonl after checkpoint creation
|
|
1602
|
+
// WU-1748 added checkpoint persistence to wu-events.jsonl but doesn't commit it,
|
|
1603
|
+
// leaving unstaged changes that cause "git rebase" to fail with "You have unstaged changes"
|
|
1604
|
+
// This restores the file to HEAD state - checkpoint data is preserved in memory store
|
|
1605
|
+
if (worktreePath) {
|
|
1606
|
+
try {
|
|
1607
|
+
execSync(`git -C "${worktreePath}" restore "${WU_EVENTS_PATH}"`);
|
|
1608
|
+
}
|
|
1609
|
+
catch {
|
|
1610
|
+
// Non-fatal: file might not exist or already clean
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
// Step 0a: Run invariants check (WU-2252: NON-BYPASSABLE, runs even with --skip-gates)
|
|
1614
|
+
// This ensures repo invariants are never violated, regardless of skip-gates flag
|
|
1615
|
+
// WU-2253: Run against worktreePath (when present) to catch violations that only exist in the worktree
|
|
1616
|
+
// WU-2425: Pass wuId to scope WU-specific invariants to just the completing WU
|
|
1617
|
+
const invariantsBaseDir = worktreePath || process.cwd();
|
|
1618
|
+
console.log(`\n${LOG_PREFIX.DONE} Running invariants check (non-bypassable)...`);
|
|
1619
|
+
console.log(`${LOG_PREFIX.DONE} Checking invariants in: ${invariantsBaseDir}`);
|
|
1620
|
+
const { runInvariants } = await import('@lumenflow/core/dist/invariants-runner.js');
|
|
1621
|
+
const invariantsResult = runInvariants({ baseDir: invariantsBaseDir, silent: false, wuId: id });
|
|
1622
|
+
if (!invariantsResult.success) {
|
|
1623
|
+
emitTelemetry({
|
|
1624
|
+
script: 'wu-done',
|
|
1625
|
+
wu_id: id,
|
|
1626
|
+
step: 'invariants',
|
|
1627
|
+
ok: false,
|
|
1628
|
+
});
|
|
1629
|
+
die(`Invariants check failed. Fix violations before completing WU.\n\n${invariantsResult.formatted}`);
|
|
1630
|
+
}
|
|
1631
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Invariants check passed`);
|
|
1632
|
+
emitTelemetry({
|
|
1633
|
+
script: 'wu-done',
|
|
1634
|
+
wu_id: id,
|
|
1635
|
+
step: 'invariants',
|
|
1636
|
+
ok: true,
|
|
1637
|
+
});
|
|
1638
|
+
// Step 0b: Run gates BEFORE merge (or skip with audit trail)
|
|
1639
|
+
if (args.skipGates) {
|
|
1640
|
+
console.log(`\n${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING} SKIP-GATES MODE ACTIVE ${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING}\n`);
|
|
1641
|
+
console.log(`${LOG_PREFIX.DONE} Skipping gates check as requested`);
|
|
1642
|
+
console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason}`);
|
|
1643
|
+
console.log(`${LOG_PREFIX.DONE} Fix WU: ${args.fixWu}`);
|
|
1644
|
+
console.log(`${LOG_PREFIX.DONE} Worktree: ${worktreePath || 'Branch-Only mode (no worktree)'}`);
|
|
1645
|
+
await auditSkipGates(id, args.reason, args.fixWu, worktreePath);
|
|
1646
|
+
console.log('\n⚠️ Ensure test failures are truly pre-existing!\n');
|
|
1647
|
+
emitTelemetry({
|
|
1648
|
+
script: 'wu-done',
|
|
1649
|
+
wu_id: id,
|
|
1650
|
+
step: 'gates',
|
|
1651
|
+
skipped: true,
|
|
1652
|
+
reason: args.reason,
|
|
1653
|
+
fix_wu: args.fixWu,
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
else if (isBranchOnly) {
|
|
1657
|
+
// Branch-Only mode: run gates in-place (current directory on lane branch)
|
|
1658
|
+
console.log(`\n${LOG_PREFIX.DONE} Running gates in Branch-Only mode (in-place on lane branch)`);
|
|
1659
|
+
const gatesCmd = isDocsOnly
|
|
1660
|
+
? `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`
|
|
1661
|
+
: `${PKG_MANAGER} ${SCRIPTS.GATES}`;
|
|
1662
|
+
if (isDocsOnly) {
|
|
1663
|
+
console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
|
|
1664
|
+
}
|
|
1665
|
+
const startTime = Date.now();
|
|
1666
|
+
try {
|
|
1667
|
+
execSync(gatesCmd, { stdio: 'inherit' });
|
|
1668
|
+
const duration = Date.now() - startTime;
|
|
1669
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
|
|
1670
|
+
emitTelemetry({
|
|
1671
|
+
script: 'wu-done',
|
|
1672
|
+
wu_id: id,
|
|
1673
|
+
step: 'gates',
|
|
1674
|
+
ok: true,
|
|
1675
|
+
duration_ms: duration,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
const duration = Date.now() - startTime;
|
|
1680
|
+
emitTelemetry({
|
|
1681
|
+
script: 'wu-done',
|
|
1682
|
+
wu_id: id,
|
|
1683
|
+
step: 'gates',
|
|
1684
|
+
ok: false,
|
|
1685
|
+
duration_ms: duration,
|
|
1686
|
+
});
|
|
1687
|
+
// WU-1280: Prominent error summary box (Branch-Only mode)
|
|
1688
|
+
// WU-1281: Extracted to helper using pretty-ms for duration formatting
|
|
1689
|
+
printGateFailureBox({
|
|
1690
|
+
id,
|
|
1691
|
+
location: 'Branch-Only',
|
|
1692
|
+
durationMs: duration,
|
|
1693
|
+
isWorktreeMode: false,
|
|
1694
|
+
});
|
|
1695
|
+
die(`Gates failed in Branch-Only mode. Fix issues and try again.`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
else if (worktreePath && existsSync(worktreePath)) {
|
|
1699
|
+
// Worktree mode: run gates in the dedicated worktree
|
|
1700
|
+
runGatesInWorktree(worktreePath, id, isDocsOnly);
|
|
1701
|
+
}
|
|
1702
|
+
else {
|
|
1703
|
+
die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
|
|
1704
|
+
`If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`);
|
|
1705
|
+
}
|
|
1706
|
+
// Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
|
|
1707
|
+
if (!args.skipCosGates) {
|
|
1708
|
+
console.log(`\n${LOG_PREFIX.DONE} Running COS governance gates...`);
|
|
1709
|
+
const startTime = Date.now();
|
|
1710
|
+
try {
|
|
1711
|
+
execSync(`${PKG_MANAGER} ${SCRIPTS.COS_GATES} ${CLI_FLAGS.WU} ${id}`, {
|
|
1712
|
+
stdio: 'inherit',
|
|
1713
|
+
});
|
|
1714
|
+
const duration = Date.now() - startTime;
|
|
1715
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} COS gates passed in ${prettyMs(duration)}`);
|
|
1716
|
+
emitTelemetry({
|
|
1717
|
+
script: 'wu-done',
|
|
1718
|
+
wu_id: id,
|
|
1719
|
+
step: 'cos-gates',
|
|
1720
|
+
ok: true,
|
|
1721
|
+
duration_ms: duration,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
catch {
|
|
1725
|
+
const duration = Date.now() - startTime;
|
|
1726
|
+
emitTelemetry({
|
|
1727
|
+
script: 'wu-done',
|
|
1728
|
+
wu_id: id,
|
|
1729
|
+
step: 'cos-gates',
|
|
1730
|
+
ok: false,
|
|
1731
|
+
duration_ms: duration,
|
|
1732
|
+
});
|
|
1733
|
+
console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} COS governance gates failed`);
|
|
1734
|
+
console.error('\nTo fix:');
|
|
1735
|
+
console.error(' 1. Add required evidence to governance.evidence field in WU YAML');
|
|
1736
|
+
console.error(' 2. See: docs/04-operations/_frameworks/cos/evidence-format.md');
|
|
1737
|
+
console.error('\nEmergency bypass (creates audit trail):');
|
|
1738
|
+
console.error(` pnpm wu:done --id ${id} --skip-cos-gates --reason "explanation"`);
|
|
1739
|
+
die('Abort: WU not completed. Fix governance evidence and retry pnpm wu:done.');
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Skipping COS governance gates as requested`);
|
|
1744
|
+
console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason || DEFAULT_NO_REASON}`);
|
|
1745
|
+
await auditSkipCosGates(id, args.reason);
|
|
1746
|
+
emitTelemetry({
|
|
1747
|
+
script: 'wu-done',
|
|
1748
|
+
wu_id: id,
|
|
1749
|
+
step: 'cos-gates',
|
|
1750
|
+
skipped: true,
|
|
1751
|
+
reason: args.reason,
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
// WU-1747: Mark checkpoint as gates passed for resumption on failure
|
|
1755
|
+
// This allows subsequent wu:done attempts to skip gates if nothing changed
|
|
1756
|
+
markGatesPassed(id);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Print State HUD for visibility
|
|
1760
|
+
* Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #4)
|
|
1761
|
+
* @param {object} params - Parameters
|
|
1762
|
+
* @param {string} params.id - WU ID
|
|
1763
|
+
* @param {object} params.docMain - Main WU YAML document
|
|
1764
|
+
* @param {boolean} params.isBranchOnly - Whether in branch-only mode
|
|
1765
|
+
* @param {boolean} params.isDocsOnly - Whether this is a docs-only WU
|
|
1766
|
+
* @param {string|null} params.derivedWorktree - Derived worktree path
|
|
1767
|
+
* @param {string} params.STAMPS_DIR - Stamps directory path
|
|
1768
|
+
*/
|
|
1769
|
+
function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR }) {
|
|
1770
|
+
const stampExists = existsSync(path.join(STAMPS_DIR, `${id}.done`)) ? 'yes' : 'no';
|
|
1771
|
+
const yamlStatus = docMain.status || 'unknown';
|
|
1772
|
+
const yamlLocked = docMain.locked === true ? 'true' : 'false';
|
|
1773
|
+
const mode = isBranchOnly ? 'branch-only' : isDocsOnly ? 'docs-only' : 'worktree';
|
|
1774
|
+
const branch = defaultBranchFrom(docMain) || 'n/a';
|
|
1775
|
+
const worktreeDisplay = isBranchOnly ? 'none' : derivedWorktree || 'none';
|
|
1776
|
+
console.log(`\n${LOG_PREFIX.DONE} HUD: WU=${id} status=${yamlStatus} stamp=${stampExists} locked=${yamlLocked} mode=${mode} branch=${branch} worktree=${worktreeDisplay}`);
|
|
1777
|
+
}
|
|
1778
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
|
|
1779
|
+
async function main() {
|
|
1780
|
+
// Validate CLI arguments and WU ID format (extracted to wu-done-validators.mjs)
|
|
1781
|
+
const { args, id } = validateInputs(process.argv);
|
|
1782
|
+
// Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
|
|
1783
|
+
const pathInfo = await detectModeAndPaths(id, args);
|
|
1784
|
+
const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain, isBranchOnly, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
|
|
1785
|
+
// Capture main checkout path once. process.cwd() may drift later during recovery flows.
|
|
1786
|
+
const mainCheckoutPath = process.cwd();
|
|
1787
|
+
// Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
|
|
1788
|
+
const preFlightResult = await executePreFlightChecks({
|
|
1789
|
+
id,
|
|
1790
|
+
args,
|
|
1791
|
+
isBranchOnly,
|
|
1792
|
+
isDocsOnly,
|
|
1793
|
+
docMain,
|
|
1794
|
+
docForValidation: initialDocForValidation,
|
|
1795
|
+
derivedWorktree,
|
|
1796
|
+
});
|
|
1797
|
+
const title = preFlightResult.title;
|
|
1798
|
+
// Note: docForValidation is returned but not used after pre-flight checks
|
|
1799
|
+
// The metadata transaction uses docForUpdate instead
|
|
1800
|
+
// Step 0: Run gates (WU-1215: extracted to executeGates function)
|
|
1801
|
+
const worktreePath = derivedWorktree && !isBranchOnly
|
|
1802
|
+
? path.isAbsolute(derivedWorktree)
|
|
1803
|
+
? derivedWorktree
|
|
1804
|
+
: path.resolve(mainCheckoutPath, derivedWorktree)
|
|
1805
|
+
: null;
|
|
1806
|
+
// WU-1943: Check if any checkpoints exist for this WU session
|
|
1807
|
+
// Warn (don't block) if no checkpoints - agent should have been checkpointing periodically
|
|
1808
|
+
try {
|
|
1809
|
+
const wuNodes = await queryByWu(worktreePath || mainCheckoutPath, id);
|
|
1810
|
+
if (!hasSessionCheckpoints(id, wuNodes)) {
|
|
1811
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: No checkpoints found for ${id} session.`);
|
|
1812
|
+
console.log(`${LOG_PREFIX.DONE} Consider using 'pnpm mem:checkpoint --wu ${id}' periodically for crash recovery.`);
|
|
1813
|
+
console.log(`${LOG_PREFIX.DONE} Checkpoint triggers: after each acceptance criterion, before gates, every 30 tool calls.\n`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
// Non-blocking: checkpoint check failure should not block wu:done
|
|
1818
|
+
}
|
|
1819
|
+
await executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath });
|
|
1820
|
+
// Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
|
|
1821
|
+
printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR });
|
|
1822
|
+
// Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
|
|
1823
|
+
// This prevents partial completion states where merge succeeds but commit fails
|
|
1824
|
+
// Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
|
|
1825
|
+
// WU-2308: Pass worktreePath to run audit from worktree (checks fixed deps, not stale main deps)
|
|
1826
|
+
const hookResult = validateAllPreCommitHooks(id, worktreePath);
|
|
1827
|
+
if (!hookResult.valid) {
|
|
1828
|
+
die('Pre-flight validation failed. Fix hook issues and try again.');
|
|
1829
|
+
}
|
|
1830
|
+
// Step 0.6: WU-1781 - Run tasks:validate preflight BEFORE any merge/push operations
|
|
1831
|
+
// This prevents deadlocks where validation fails after merge, leaving local main ahead of origin
|
|
1832
|
+
// Specifically catches stamp-status mismatches from legacy WUs that would block pre-push hooks
|
|
1833
|
+
const tasksValidationResult = runPreflightTasksValidation(id);
|
|
1834
|
+
if (!tasksValidationResult.valid) {
|
|
1835
|
+
const errorMessage = buildPreflightErrorMessage(id, tasksValidationResult.errors);
|
|
1836
|
+
console.error(errorMessage);
|
|
1837
|
+
die('Preflight tasks:validate failed. See errors above for fix options.');
|
|
1838
|
+
}
|
|
1839
|
+
// Step 1: Execute mode-specific completion workflow (WU-1215: extracted to mode modules)
|
|
1840
|
+
// Worktree mode: Update metadata in worktree → commit → merge to main
|
|
1841
|
+
// Branch-Only mode: Merge to main → update metadata on main → commit
|
|
1842
|
+
// WU-1811: Track cleanupSafe flag to conditionally skip worktree removal on failure
|
|
1843
|
+
let completionResult = { cleanupSafe: true }; // Default to safe for no-auto mode
|
|
1844
|
+
if (!args.noAuto) {
|
|
1845
|
+
// Build context for mode-specific execution
|
|
1846
|
+
// WU-1369: Worktree mode uses atomic transaction pattern (no recordTransactionState/rollbackTransaction)
|
|
1847
|
+
// Branch-only mode still uses the old rollback mechanism
|
|
1848
|
+
const baseContext = {
|
|
1849
|
+
id,
|
|
1850
|
+
args,
|
|
1851
|
+
docMain,
|
|
1852
|
+
title,
|
|
1853
|
+
isDocsOnly,
|
|
1854
|
+
maxCommitLength: getCommitHeaderLimit(),
|
|
1855
|
+
validateStagedFiles,
|
|
1856
|
+
};
|
|
1857
|
+
try {
|
|
1858
|
+
if (isBranchOnly) {
|
|
1859
|
+
// Branch-Only mode: merge first, then update metadata on main
|
|
1860
|
+
// NOTE: Branch-only still uses old rollback mechanism
|
|
1861
|
+
const branchOnlyContext = {
|
|
1862
|
+
...baseContext,
|
|
1863
|
+
recordTransactionState,
|
|
1864
|
+
rollbackTransaction,
|
|
1865
|
+
};
|
|
1866
|
+
completionResult = await executeBranchOnlyCompletion(branchOnlyContext);
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
// Worktree mode: update in worktree, commit, then merge or create PR
|
|
1870
|
+
// WU-1369: Uses atomic transaction pattern
|
|
1871
|
+
const worktreeContext = {
|
|
1872
|
+
...baseContext,
|
|
1873
|
+
worktreePath,
|
|
1874
|
+
};
|
|
1875
|
+
completionResult = await executeWorktreeCompletion(worktreeContext);
|
|
1876
|
+
}
|
|
1877
|
+
// Handle recovery mode (zombie state cleanup completed)
|
|
1878
|
+
if ('recovered' in completionResult && completionResult.recovered) {
|
|
1879
|
+
// P0 FIX: Release lane lock before early exit
|
|
1880
|
+
try {
|
|
1881
|
+
const lane = docMain.lane;
|
|
1882
|
+
if (lane)
|
|
1883
|
+
releaseLaneLock(lane, { wuId: id });
|
|
1884
|
+
}
|
|
1885
|
+
catch { }
|
|
1886
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
catch (err) {
|
|
1890
|
+
// P0 FIX: Release lane lock before error exit
|
|
1891
|
+
try {
|
|
1892
|
+
const lane = docMain.lane;
|
|
1893
|
+
if (lane)
|
|
1894
|
+
releaseLaneLock(lane, { wuId: id });
|
|
1895
|
+
}
|
|
1896
|
+
catch { }
|
|
1897
|
+
// WU-1811: Check if cleanup is safe before removing worktree
|
|
1898
|
+
// If cleanupSafe is false (or undefined), preserve worktree for recovery
|
|
1899
|
+
if (err.cleanupSafe === false) {
|
|
1900
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Worktree preserved - rerun wu:done to recover`);
|
|
1901
|
+
}
|
|
1902
|
+
// Mode modules handle rollback internally, we just need to exit
|
|
1903
|
+
// Exit code 1 = recoverable (rebase/fix and retry)
|
|
1904
|
+
process.exit(EXIT_CODES.ERROR);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
|
|
1909
|
+
}
|
|
1910
|
+
// Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
|
|
1911
|
+
// WU-1811: Only run cleanup if all completion steps succeeded
|
|
1912
|
+
if (completionResult.cleanupSafe !== false) {
|
|
1913
|
+
await runCleanup(docMain, args);
|
|
1914
|
+
}
|
|
1915
|
+
else {
|
|
1916
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Skipping worktree cleanup - metadata/push incomplete`);
|
|
1917
|
+
}
|
|
1918
|
+
// WU-1603: Release lane lock after successful completion
|
|
1919
|
+
// This allows the lane to be claimed by another WU
|
|
1920
|
+
try {
|
|
1921
|
+
const lane = docMain.lane;
|
|
1922
|
+
if (lane) {
|
|
1923
|
+
const releaseResult = releaseLaneLock(lane, { wuId: id });
|
|
1924
|
+
if (releaseResult.released && !releaseResult.notFound) {
|
|
1925
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
|
|
1926
|
+
}
|
|
1927
|
+
// Silent if notFound - lock may not exist (older WUs, manual cleanup)
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
catch (err) {
|
|
1931
|
+
// Non-blocking: lock release failure should not block completion
|
|
1932
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${err.message}`);
|
|
1933
|
+
}
|
|
1934
|
+
// WU-1438: Auto-end agent session
|
|
1935
|
+
try {
|
|
1936
|
+
const sessionResult = endSessionForWU();
|
|
1937
|
+
if (sessionResult.ended) {
|
|
1938
|
+
// Emergency fix Session 2: Use SESSION.ID_DISPLAY_LENGTH constant
|
|
1939
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Agent session ended (${sessionResult.summary.session_id.slice(0, SESSION.ID_DISPLAY_LENGTH)}...)`);
|
|
1940
|
+
}
|
|
1941
|
+
// No warning if no active session - silent no-op is expected
|
|
1942
|
+
}
|
|
1943
|
+
catch (err) {
|
|
1944
|
+
// Non-blocking: session end failure should not block completion
|
|
1945
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not end agent session: ${err.message}`);
|
|
1946
|
+
}
|
|
1947
|
+
// WU-1588: Broadcast completion signal after session end
|
|
1948
|
+
// Non-blocking: failures handled internally by broadcastCompletionSignal
|
|
1949
|
+
await broadcastCompletionSignal(id, title);
|
|
1950
|
+
// WU-1946: Update spawn registry to mark WU as completed
|
|
1951
|
+
// Non-blocking: failures handled internally by updateSpawnRegistryOnCompletion
|
|
1952
|
+
// Works in both worktree and branch-only modes (called after completionResult)
|
|
1953
|
+
await updateSpawnRegistryOnCompletion(id, mainCheckoutPath);
|
|
1954
|
+
// WU-1747: Clear checkpoint on successful completion
|
|
1955
|
+
// Checkpoint is no longer needed once WU is fully complete
|
|
1956
|
+
clearCheckpoint(id);
|
|
1957
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
|
|
1958
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
|
|
1959
|
+
console.log(`- WU: ${id} — ${title}`);
|
|
1960
|
+
// WU-1763: Print lifecycle nudges (conditional, non-blocking)
|
|
1961
|
+
// Discovery summary nudge - only if discoveries exist
|
|
1962
|
+
const discoveries = await loadDiscoveriesForWU(mainCheckoutPath, id);
|
|
1963
|
+
printDiscoveryNudge(id, discoveries.count, discoveries.ids);
|
|
1964
|
+
// Documentation validation nudge - only if docs changed
|
|
1965
|
+
// Use worktreePath if available, otherwise skip (branch-only mode has no worktree)
|
|
1966
|
+
if (worktreePath) {
|
|
1967
|
+
const changedDocs = await detectChangedDocPaths(worktreePath, BRANCHES.MAIN);
|
|
1968
|
+
printDocValidationNudge(id, changedDocs);
|
|
1969
|
+
}
|
|
1970
|
+
// WU-1983: Migration deployment nudge - only if supabase paths in code_paths
|
|
1971
|
+
const codePaths = docMain.code_paths || [];
|
|
1972
|
+
await printMigrationDeploymentNudge(codePaths, mainCheckoutPath);
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* WU-1983: Print migration deployment nudge when WU includes supabase changes.
|
|
1976
|
+
* Notifies agent to deploy new migrations to production via MCP tools.
|
|
1977
|
+
* Conditional output - only prints when migrations are in scope and new migrations exist.
|
|
1978
|
+
*
|
|
1979
|
+
* @param {string[]} codePaths - WU code_paths array
|
|
1980
|
+
* @param {string} baseDir - Base directory for migration discovery
|
|
1981
|
+
* @returns {Promise<void>}
|
|
1982
|
+
*/
|
|
1983
|
+
export async function printMigrationDeploymentNudge(codePaths, baseDir) {
|
|
1984
|
+
// Only check if WU includes supabase paths
|
|
1985
|
+
if (!hasMigrationChanges(codePaths)) {
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
const { files: migrations, errors } = discoverLocalMigrations(baseDir);
|
|
1990
|
+
if (errors.length > 0) {
|
|
1991
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Migration discovery errors: ${errors.join(', ')}`);
|
|
1992
|
+
}
|
|
1993
|
+
if (migrations.length > 0) {
|
|
1994
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.INFO} WU includes supabase migrations.`);
|
|
1995
|
+
console.log(`${LOG_PREFIX.DONE} ${migrations.length} local migration(s) detected.`);
|
|
1996
|
+
console.log(`${LOG_PREFIX.DONE} To sync with production:`);
|
|
1997
|
+
console.log(` 1. mcp__supabase__list_migrations (check production state)`);
|
|
1998
|
+
console.log(` 2. pnpm db:sync --production-file <output.json> (detect drift)`);
|
|
1999
|
+
console.log(` 3. mcp__supabase__apply_migration (deploy new migrations)`);
|
|
2000
|
+
console.log(` See: docs/02-technical/database/migration-workflow.md\n`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
catch (err) {
|
|
2004
|
+
// Non-blocking: migration check failure should not block wu:done
|
|
2005
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check migrations: ${err.message}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* WU-1763: Print discovery summary nudge when discoveries exist for this WU.
|
|
2010
|
+
* Conditional output - only prints when discoveryCount > 0.
|
|
2011
|
+
* Non-blocking, single-line output to avoid flooding the console.
|
|
2012
|
+
*
|
|
2013
|
+
* @param {string} id - WU ID being completed
|
|
2014
|
+
* @param {number} discoveryCount - Number of open discoveries for this WU
|
|
2015
|
+
* @param {string[]} discoveryIds - List of discovery IDs (limited to 5 in output)
|
|
2016
|
+
*/
|
|
2017
|
+
export function printDiscoveryNudge(id, discoveryCount, discoveryIds) {
|
|
2018
|
+
if (discoveryCount > 0) {
|
|
2019
|
+
const displayIds = discoveryIds.slice(0, 5).join(', ');
|
|
2020
|
+
const moreText = discoveryCount > 5 ? ` (+${discoveryCount - 5} more)` : '';
|
|
2021
|
+
console.log(`\n${LOG_PREFIX.DONE} 💡 ${discoveryCount} open discoveries: ${displayIds}${moreText}`);
|
|
2022
|
+
console.log(` Triage with: pnpm mem:triage --wu ${id}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* WU-1763: Print documentation validation nudge when docs changed.
|
|
2027
|
+
* Conditional output - only prints when changedDocPaths.length > 0.
|
|
2028
|
+
* Non-blocking, single-line output to avoid flooding the console.
|
|
2029
|
+
*
|
|
2030
|
+
* @param {string} id - WU ID being completed
|
|
2031
|
+
* @param {string[]} changedDocPaths - List of documentation paths that changed
|
|
2032
|
+
*/
|
|
2033
|
+
export function printDocValidationNudge(id, changedDocPaths) {
|
|
2034
|
+
if (changedDocPaths.length > 0) {
|
|
2035
|
+
console.log(`\n${LOG_PREFIX.DONE} 💡 Documentation changed (${changedDocPaths.length} files).`);
|
|
2036
|
+
console.log(` Consider: pnpm validate:context && pnpm docs:linkcheck`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* WU-1763: Load discoveries for a WU from memory store.
|
|
2041
|
+
* Non-blocking - returns empty array on errors.
|
|
2042
|
+
*
|
|
2043
|
+
* @param {string} baseDir - Base directory containing .beacon/memory/
|
|
2044
|
+
* @param {string} wuId - WU ID to load discoveries for
|
|
2045
|
+
* @returns {Promise<{count: number, ids: string[]}>} Discovery count and IDs
|
|
2046
|
+
*/
|
|
2047
|
+
async function loadDiscoveriesForWU(baseDir, wuId) {
|
|
2048
|
+
try {
|
|
2049
|
+
const memory = await loadMemory(path.join(baseDir, '.beacon/memory'));
|
|
2050
|
+
const wuNodes = memory.byWu.get(wuId) || [];
|
|
2051
|
+
const discoveries = wuNodes.filter((node) => node.type === 'discovery');
|
|
2052
|
+
return {
|
|
2053
|
+
count: discoveries.length,
|
|
2054
|
+
ids: discoveries.map((d) => d.id),
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
catch {
|
|
2058
|
+
// Non-blocking: return empty on errors
|
|
2059
|
+
return { count: 0, ids: [] };
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* WU-1763: Detect documentation paths from changed files.
|
|
2064
|
+
* Non-blocking - returns empty array on errors.
|
|
2065
|
+
*
|
|
2066
|
+
* @param {string} worktreePath - Path to worktree
|
|
2067
|
+
* @param {string} baseBranch - Base branch to compare against
|
|
2068
|
+
* @returns {Promise<string[]>} List of changed documentation paths
|
|
2069
|
+
*/
|
|
2070
|
+
async function detectChangedDocPaths(worktreePath, baseBranch) {
|
|
2071
|
+
try {
|
|
2072
|
+
const git = getGitForCwd();
|
|
2073
|
+
// Get files changed in this branch vs base
|
|
2074
|
+
const diff = await git.raw(['diff', '--name-only', baseBranch]);
|
|
2075
|
+
const changedFiles = diff.split('\n').filter(Boolean);
|
|
2076
|
+
// Filter to docs: ai/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
|
|
2077
|
+
const docPatterns = [
|
|
2078
|
+
/^ai\/onboarding\//,
|
|
2079
|
+
/^docs\//,
|
|
2080
|
+
/^\.claude\//,
|
|
2081
|
+
/^CLAUDE\.md$/,
|
|
2082
|
+
/^README\.md$/,
|
|
2083
|
+
];
|
|
2084
|
+
return changedFiles.filter((f) => docPatterns.some((p) => p.test(f)));
|
|
2085
|
+
}
|
|
2086
|
+
catch {
|
|
2087
|
+
// Non-blocking: return empty on errors
|
|
2088
|
+
return [];
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
// Guard main() execution for testability (WU-1366)
|
|
2092
|
+
// When imported as a module for testing, main() should not auto-run
|
|
2093
|
+
import { fileURLToPath } from 'node:url';
|
|
2094
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
2095
|
+
main();
|
|
2096
|
+
}
|