@lumenflow/cli 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Delete Helper
4
+ *
5
+ * Race-safe WU deletion using micro-worktree isolation.
6
+ *
7
+ * Uses micro-worktree pattern:
8
+ * 1) Validate inputs (WU exists, status is not in_progress)
9
+ * 2) Ensure main is clean and up-to-date with origin
10
+ * 3) Create temp branch WITHOUT switching (main checkout stays on main)
11
+ * 4) Create micro-worktree in /tmp pointing to temp branch
12
+ * 5) Delete WU file and update backlog.md in micro-worktree
13
+ * 6) Commit, ff-only merge, push
14
+ * 7) Cleanup temp branch and micro-worktree
15
+ *
16
+ * Usage:
17
+ * pnpm wu:delete --id WU-123 # Single WU deletion
18
+ * pnpm wu:delete --id WU-123 --dry-run # Dry run
19
+ * pnpm wu:delete --batch WU-1,WU-2,WU-3 # Batch deletion
20
+ */
21
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
24
+ import { die } from '@lumenflow/core/dist/error-handler.js';
25
+ import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
26
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
27
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
28
+ import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
29
+ import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat, } from '@lumenflow/core/dist/wu-helpers.js';
30
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
31
+ const PREFIX = LOG_PREFIX.DELETE || '[wu:delete]';
32
+ const DELETE_OPTIONS = {
33
+ dryRun: {
34
+ name: 'dryRun',
35
+ flags: '--dry-run',
36
+ description: 'Show what would be deleted without making changes',
37
+ },
38
+ batch: {
39
+ name: 'batch',
40
+ flags: '--batch <ids>',
41
+ description: 'Delete multiple WUs atomically (comma-separated: WU-1,WU-2,WU-3)',
42
+ },
43
+ };
44
+ function parseArgs() {
45
+ return createWUParser({
46
+ name: 'wu-delete',
47
+ description: 'Safely delete WU YAML files with micro-worktree isolation',
48
+ options: [WU_OPTIONS.id, DELETE_OPTIONS.dryRun, DELETE_OPTIONS.batch],
49
+ required: [],
50
+ allowPositionalId: true,
51
+ });
52
+ }
53
+ function parseBatchIds(batchArg) {
54
+ return batchArg
55
+ .split(',')
56
+ .map((id) => id.trim().toUpperCase())
57
+ .filter((id) => id.length > 0);
58
+ }
59
+ function validateWUDeletable(id) {
60
+ const wuPath = WU_PATHS.WU(id);
61
+ if (!existsSync(wuPath)) {
62
+ die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
63
+ }
64
+ const content = readFileSync(wuPath, FILE_SYSTEM.ENCODING);
65
+ const wu = parseYAML(content);
66
+ if (wu.status === WU_STATUS.IN_PROGRESS) {
67
+ die(`Cannot delete WU ${id}: status is '${WU_STATUS.IN_PROGRESS}'.\n\n` +
68
+ `WUs that are actively being worked on cannot be deleted.\n` +
69
+ `If the WU was abandoned, first run: pnpm wu:block --id ${id} --reason "Abandoned"\n` +
70
+ `Then retry the delete operation.`);
71
+ }
72
+ return { wu, wuPath };
73
+ }
74
+ async function ensureCleanWorkingTree() {
75
+ const status = await getGitForCwd().getStatus();
76
+ if (status.trim()) {
77
+ die(`Working tree is not clean. Cannot delete WU.\n\nUncommitted changes:\n${status}\n\nCommit or stash changes before deleting:\n git add . && git commit -m "..."\n`);
78
+ }
79
+ }
80
+ function getStampPath(id) {
81
+ return join(WU_PATHS.STAMPS_DIR(), `${id}.done`);
82
+ }
83
+ function stampExists(id) {
84
+ return existsSync(getStampPath(id));
85
+ }
86
+ function removeFromBacklog(backlogPath, id) {
87
+ if (!existsSync(backlogPath)) {
88
+ return false;
89
+ }
90
+ const content = readFileSync(backlogPath, FILE_SYSTEM.ENCODING);
91
+ const wuLinkPattern = new RegExp(`^.*\\[${id}[^\\]]*\\].*$`, 'gmi');
92
+ const wuSimplePattern = new RegExp(`^.*${id}.*\\.yaml.*$`, 'gmi');
93
+ let updated = content.replace(wuLinkPattern, '');
94
+ updated = updated.replace(wuSimplePattern, '');
95
+ updated = updated.replace(/\n{3,}/g, '\n\n');
96
+ if (updated !== content) {
97
+ writeFileSync(backlogPath, updated, FILE_SYSTEM.ENCODING);
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ async function deleteSingleWU(id, dryRun) {
103
+ console.log(`${PREFIX} Starting WU delete for ${id}`);
104
+ validateWUIDFormat(id);
105
+ const { wu, wuPath } = validateWUDeletable(id);
106
+ console.log(`${PREFIX} WU details:`);
107
+ console.log(`${PREFIX} Title: ${wu.title}`);
108
+ console.log(`${PREFIX} Lane: ${wu.lane}`);
109
+ console.log(`${PREFIX} Status: ${wu.status}`);
110
+ console.log(`${PREFIX} Path: ${wuPath}`);
111
+ if (dryRun) {
112
+ console.log(`\n${PREFIX} 🔍 DRY RUN: Would delete ${id}`);
113
+ console.log(`${PREFIX} - Delete file: ${wuPath}`);
114
+ console.log(`${PREFIX} - Update: ${WU_PATHS.BACKLOG()}`);
115
+ if (stampExists(id)) {
116
+ console.log(`${PREFIX} - Delete stamp: ${getStampPath(id)}`);
117
+ }
118
+ console.log(`${PREFIX} No changes made.`);
119
+ process.exit(EXIT_CODES.SUCCESS);
120
+ }
121
+ await ensureOnMain(getGitForCwd());
122
+ await ensureCleanWorkingTree();
123
+ await ensureMainUpToDate(getGitForCwd(), 'wu:delete');
124
+ console.log(`${PREFIX} Deleting via micro-worktree...`);
125
+ await withMicroWorktree({
126
+ operation: MICRO_WORKTREE_OPERATIONS.WU_DELETE,
127
+ id: id,
128
+ logPrefix: PREFIX,
129
+ execute: async ({ worktreePath, gitWorktree }) => {
130
+ const wuFilePath = join(worktreePath, wuPath);
131
+ const backlogFilePath = join(worktreePath, WU_PATHS.BACKLOG());
132
+ unlinkSync(wuFilePath);
133
+ console.log(`${PREFIX} ✅ Deleted ${id}.yaml`);
134
+ const stampPath = join(worktreePath, getStampPath(id));
135
+ if (existsSync(stampPath)) {
136
+ unlinkSync(stampPath);
137
+ console.log(`${PREFIX} ✅ Deleted stamp ${id}.done`);
138
+ }
139
+ const removedFromBacklog = removeFromBacklog(backlogFilePath, id);
140
+ if (removedFromBacklog) {
141
+ console.log(`${PREFIX} ✅ Removed ${id} from backlog.md`);
142
+ }
143
+ else {
144
+ console.log(`${PREFIX} ℹ️ ${id} was not found in backlog.md`);
145
+ }
146
+ await gitWorktree.add('.');
147
+ const commitMessage = `docs: delete ${id.toLowerCase()}`;
148
+ return {
149
+ commitMessage,
150
+ files: [],
151
+ };
152
+ },
153
+ });
154
+ console.log(`${PREFIX} ✅ Successfully deleted ${id}`);
155
+ console.log(`${PREFIX} Changes pushed to origin/main`);
156
+ }
157
+ async function deleteBatchWUs(ids, dryRun) {
158
+ console.log(`${PREFIX} Starting batch delete for ${ids.length} WU(s): ${ids.join(', ')}`);
159
+ const wusToDelete = [];
160
+ const stampsToDelete = [];
161
+ for (const id of ids) {
162
+ validateWUIDFormat(id);
163
+ const { wu, wuPath } = validateWUDeletable(id);
164
+ wusToDelete.push({ id, wu, wuPath });
165
+ if (stampExists(id)) {
166
+ stampsToDelete.push(id);
167
+ }
168
+ }
169
+ console.log(`${PREFIX} WUs to delete:`);
170
+ for (const { id, wu, wuPath } of wusToDelete) {
171
+ console.log(`${PREFIX} ${id}: ${wu.title} (${wu.status}) - ${wuPath}`);
172
+ }
173
+ if (dryRun) {
174
+ console.log(`\n${PREFIX} 🔍 DRY RUN: Would delete ${ids.length} WU(s)`);
175
+ console.log(`${PREFIX} No changes made.`);
176
+ process.exit(EXIT_CODES.SUCCESS);
177
+ }
178
+ await ensureOnMain(getGitForCwd());
179
+ await ensureCleanWorkingTree();
180
+ await ensureMainUpToDate(getGitForCwd(), 'wu:delete --batch');
181
+ console.log(`${PREFIX} Deleting ${ids.length} WU(s) via micro-worktree...`);
182
+ await withMicroWorktree({
183
+ operation: MICRO_WORKTREE_OPERATIONS.WU_DELETE,
184
+ id: `batch-${ids.length}`,
185
+ logPrefix: PREFIX,
186
+ execute: async ({ worktreePath, gitWorktree }) => {
187
+ const backlogFilePath = join(worktreePath, WU_PATHS.BACKLOG());
188
+ for (const { id, wuPath } of wusToDelete) {
189
+ const wuFilePath = join(worktreePath, wuPath);
190
+ unlinkSync(wuFilePath);
191
+ console.log(`${PREFIX} ✅ Deleted ${id}.yaml`);
192
+ }
193
+ for (const id of stampsToDelete) {
194
+ const stampPath = join(worktreePath, getStampPath(id));
195
+ if (existsSync(stampPath)) {
196
+ unlinkSync(stampPath);
197
+ console.log(`${PREFIX} ✅ Deleted stamp ${id}.done`);
198
+ }
199
+ }
200
+ for (const { id } of wusToDelete) {
201
+ const removed = removeFromBacklog(backlogFilePath, id);
202
+ if (removed) {
203
+ console.log(`${PREFIX} ✅ Removed ${id} from backlog.md`);
204
+ }
205
+ }
206
+ await gitWorktree.add('.');
207
+ const idList = ids.map((id) => id.toLowerCase()).join(', ');
208
+ const commitMessage = `chore(repair): delete ${ids.length} orphaned wus (${idList})`;
209
+ return {
210
+ commitMessage,
211
+ files: [],
212
+ };
213
+ },
214
+ });
215
+ console.log(`${PREFIX} ✅ Successfully deleted ${ids.length} WU(s)`);
216
+ console.log(`${PREFIX} Changes pushed to origin/main`);
217
+ }
218
+ async function main() {
219
+ const opts = parseArgs();
220
+ const { id, dryRun, batch } = opts;
221
+ if (!id && !batch) {
222
+ die('Must specify either --id WU-XXX or --batch WU-1,WU-2,WU-3');
223
+ }
224
+ if (id && batch) {
225
+ die('Cannot use both --id and --batch. Use one or the other.');
226
+ }
227
+ if (batch) {
228
+ const ids = parseBatchIds(batch);
229
+ if (ids.length === 0) {
230
+ die('--batch requires at least one WU ID');
231
+ }
232
+ await deleteBatchWUs(ids, dryRun);
233
+ }
234
+ else {
235
+ await deleteSingleWU(id, dryRun);
236
+ }
237
+ }
238
+ main().catch((err) => {
239
+ console.error(`${PREFIX} ❌ ${err.message}`);
240
+ process.exit(EXIT_CODES.ERROR);
241
+ });
package/dist/wu-done.js CHANGED
@@ -147,7 +147,7 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
147
147
  ` pnpm wu:repair-claim --id ${id}\n\n` +
148
148
  `After repair, retry:\n` +
149
149
  ` pnpm wu:done --id ${id}\n\n` +
150
- `See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.`);
150
+ `See: docs/04-operations/_frameworks/lumenflow/agent/onboarding/troubleshooting-wu-done.md for more recovery options.`);
151
151
  }
152
152
  export function printExposureWarnings(wu, options = {}) {
153
153
  // Validate exposure
@@ -174,6 +174,77 @@ export function validateAccessibilityOrDie(wu, options = {}) {
174
174
  `This gate prevents "orphaned code" - features that exist but users cannot access.`);
175
175
  }
176
176
  }
177
+ export function validateDocsOnlyFlag(wu, args) {
178
+ // If --docs-only flag is not used, no validation needed
179
+ if (!args.docsOnly) {
180
+ return { valid: true, errors: [] };
181
+ }
182
+ const wuId = wu.id || 'unknown';
183
+ const exposure = wu.exposure;
184
+ const type = wu.type;
185
+ const codePaths = wu.code_paths;
186
+ // Check 1: exposure is 'documentation'
187
+ if (exposure === 'documentation') {
188
+ return { valid: true, errors: [] };
189
+ }
190
+ // Check 2: type is 'documentation'
191
+ if (type === 'documentation') {
192
+ return { valid: true, errors: [] };
193
+ }
194
+ // Check 3: all code_paths are documentation paths
195
+ const DOCS_ONLY_PREFIXES = ['docs/', 'ai/', '.claude/', 'memory-bank/'];
196
+ const DOCS_ONLY_ROOT_FILES = ['readme', 'claude'];
197
+ const isDocsPath = (p) => {
198
+ const path = p.trim().toLowerCase();
199
+ // Check docs prefixes
200
+ for (const prefix of DOCS_ONLY_PREFIXES) {
201
+ if (path.startsWith(prefix))
202
+ return true;
203
+ }
204
+ // Check markdown files
205
+ if (path.endsWith('.md'))
206
+ return true;
207
+ // Check root file patterns
208
+ for (const pattern of DOCS_ONLY_ROOT_FILES) {
209
+ if (path.startsWith(pattern))
210
+ return true;
211
+ }
212
+ return false;
213
+ };
214
+ if (codePaths && Array.isArray(codePaths) && codePaths.length > 0) {
215
+ const allDocsOnly = codePaths.every((p) => typeof p === 'string' && isDocsPath(p));
216
+ if (allDocsOnly) {
217
+ return { valid: true, errors: [] };
218
+ }
219
+ }
220
+ // Validation failed - provide clear error message
221
+ const currentExposure = exposure || 'not set';
222
+ const currentType = type || 'not set';
223
+ return {
224
+ valid: false,
225
+ errors: [
226
+ `--docs-only flag used on ${wuId} but WU is not documentation-focused.\n\n` +
227
+ `Current exposure: ${currentExposure}\n` +
228
+ `Current type: ${currentType}\n\n` +
229
+ `--docs-only requires one of:\n` +
230
+ ` 1. exposure: documentation\n` +
231
+ ` 2. type: documentation\n` +
232
+ ` 3. All code_paths under docs/, ai/, .claude/, or *.md files\n\n` +
233
+ `To fix, either:\n` +
234
+ ` - Remove --docs-only flag and run full gates\n` +
235
+ ` - Change WU exposure to 'documentation' if this is truly a docs-only change`,
236
+ ],
237
+ };
238
+ }
239
+ export function buildGatesCommand(options) {
240
+ const { docsOnly = false, isDocsOnly = false } = options;
241
+ // Use docs-only gates if either explicit flag or auto-detected
242
+ const shouldUseDocsOnly = docsOnly || isDocsOnly;
243
+ if (shouldUseDocsOnly) {
244
+ return `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`;
245
+ }
246
+ return `${PKG_MANAGER} ${SCRIPTS.GATES}`;
247
+ }
177
248
  async function assertWorktreeWUInProgressInStateStore(id, worktreePath) {
178
249
  const resolvedWorktreePath = path.resolve(worktreePath);
179
250
  const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
@@ -810,15 +881,27 @@ function checkNodeModulesStaleness(worktreePath) {
810
881
  console.warn(`${LOG_PREFIX.DONE} Could not check node_modules staleness: ${e.message}`);
811
882
  }
812
883
  }
813
- function runGatesInWorktree(worktreePath, id, isDocsOnly = false) {
884
+ /**
885
+ * Run gates in worktree
886
+ * @param {string} worktreePath - Path to worktree
887
+ * @param {string} id - WU ID
888
+ * @param {object} options - Gates options
889
+ * @param {boolean} options.isDocsOnly - Auto-detected docs-only from code_paths
890
+ * @param {boolean} options.docsOnly - Explicit --docs-only flag from CLI
891
+ */
892
+ function runGatesInWorktree(worktreePath, id, options = {}) {
893
+ const { isDocsOnly = false, docsOnly = false } = options;
814
894
  console.log(`\n${LOG_PREFIX.DONE} Running gates in worktree: ${worktreePath}`);
815
895
  // Check for stale node_modules before running gates (prevents confusing failures)
816
896
  checkNodeModulesStaleness(worktreePath);
817
- const gatesCmd = isDocsOnly
818
- ? `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`
819
- : `${PKG_MANAGER} ${SCRIPTS.GATES}`;
820
- if (isDocsOnly) {
897
+ // WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
898
+ const useDocsOnlyGates = docsOnly || isDocsOnly;
899
+ const gatesCmd = buildGatesCommand({ docsOnly, isDocsOnly });
900
+ if (useDocsOnlyGates) {
821
901
  console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
902
+ if (docsOnly) {
903
+ console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
904
+ }
822
905
  }
823
906
  const startTime = Date.now();
824
907
  try {
@@ -1544,6 +1627,11 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1544
1627
  console.warn(`\nThis is a NON-BLOCKING warning.`);
1545
1628
  console.warn(`Use --require-agents to make this a blocking error.\n`);
1546
1629
  }
1630
+ // WU-1012: Validate --docs-only flag usage (BLOCKING)
1631
+ const docsOnlyValidation = validateDocsOnlyFlag(docForValidation, { docsOnly: args.docsOnly });
1632
+ if (!docsOnlyValidation.valid) {
1633
+ die(docsOnlyValidation.errors[0]);
1634
+ }
1547
1635
  // WU-1999: Exposure validation (NON-BLOCKING warning)
1548
1636
  printExposureWarnings(docForValidation, { skipExposureCheck: args.skipExposureCheck });
1549
1637
  // WU-2022: Feature accessibility validation (BLOCKING)
@@ -1656,11 +1744,14 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1656
1744
  else if (isBranchOnly) {
1657
1745
  // Branch-Only mode: run gates in-place (current directory on lane branch)
1658
1746
  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) {
1747
+ // WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
1748
+ const useDocsOnlyGates = args.docsOnly || isDocsOnly;
1749
+ const gatesCmd = buildGatesCommand({ docsOnly: Boolean(args.docsOnly), isDocsOnly });
1750
+ if (useDocsOnlyGates) {
1663
1751
  console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
1752
+ if (args.docsOnly) {
1753
+ console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
1754
+ }
1664
1755
  }
1665
1756
  const startTime = Date.now();
1666
1757
  try {
@@ -1697,11 +1788,13 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1697
1788
  }
1698
1789
  else if (worktreePath && existsSync(worktreePath)) {
1699
1790
  // Worktree mode: run gates in the dedicated worktree
1700
- runGatesInWorktree(worktreePath, id, isDocsOnly);
1791
+ // WU-1012: Pass both auto-detected and explicit docs-only flags
1792
+ runGatesInWorktree(worktreePath, id, { isDocsOnly, docsOnly: Boolean(args.docsOnly) });
1701
1793
  }
1702
1794
  else {
1703
1795
  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.`);
1796
+ `If the worktree was removed, recreate it and retry, or rerun with --branch-only when the lane branch exists.\n` +
1797
+ `Use --skip-gates only with justification.`);
1705
1798
  }
1706
1799
  // Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
1707
1800
  if (!args.skipCosGates) {
@@ -1766,6 +1859,13 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1766
1859
  * @param {string|null} params.derivedWorktree - Derived worktree path
1767
1860
  * @param {string} params.STAMPS_DIR - Stamps directory path
1768
1861
  */
1862
+ export function computeBranchOnlyFallback({ isBranchOnly, branchOnlyRequested, worktreeExists, derivedWorktree, }) {
1863
+ const allowFallback = Boolean(branchOnlyRequested) && !isBranchOnly && !worktreeExists && Boolean(derivedWorktree);
1864
+ return {
1865
+ allowFallback,
1866
+ effectiveBranchOnly: isBranchOnly || allowFallback,
1867
+ };
1868
+ }
1769
1869
  function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR }) {
1770
1870
  const stampExists = existsSync(path.join(STAMPS_DIR, `${id}.done`)) ? 'yes' : 'no';
1771
1871
  const yamlStatus = docMain.status || 'unknown';
@@ -1777,6 +1877,8 @@ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree,
1777
1877
  }
1778
1878
  // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1779
1879
  async function main() {
1880
+ // Allow pre-push hook to recognize wu:done automation (WU-1030)
1881
+ process.env.LUMENFLOW_WU_TOOL = 'wu-done';
1780
1882
  // Validate CLI arguments and WU ID format (extracted to wu-done-validators.mjs)
1781
1883
  const { args, id } = validateInputs(process.argv);
1782
1884
  // Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
@@ -1784,25 +1886,39 @@ async function main() {
1784
1886
  const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain, isBranchOnly, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
1785
1887
  // Capture main checkout path once. process.cwd() may drift later during recovery flows.
1786
1888
  const mainCheckoutPath = process.cwd();
1889
+ // Resolve worktree path early so we can detect missing worktree before pre-flight checks
1890
+ const resolvedWorktreePath = derivedWorktree && !isBranchOnly
1891
+ ? path.isAbsolute(derivedWorktree)
1892
+ ? derivedWorktree
1893
+ : path.resolve(mainCheckoutPath, derivedWorktree)
1894
+ : null;
1895
+ const worktreeExists = resolvedWorktreePath ? existsSync(resolvedWorktreePath) : false;
1896
+ const { allowFallback: allowBranchOnlyFallback, effectiveBranchOnly } = computeBranchOnlyFallback({
1897
+ isBranchOnly,
1898
+ branchOnlyRequested: args.branchOnly,
1899
+ worktreeExists,
1900
+ derivedWorktree,
1901
+ });
1902
+ if (allowBranchOnlyFallback) {
1903
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree missing (${resolvedWorktreePath}). Proceeding in branch-only mode because --branch-only was provided.`);
1904
+ }
1905
+ const effectiveDerivedWorktree = effectiveBranchOnly ? null : derivedWorktree;
1906
+ const effectiveWorktreePath = effectiveBranchOnly ? null : resolvedWorktreePath;
1787
1907
  // Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
1788
1908
  const preFlightResult = await executePreFlightChecks({
1789
1909
  id,
1790
1910
  args,
1791
- isBranchOnly,
1911
+ isBranchOnly: effectiveBranchOnly,
1792
1912
  isDocsOnly,
1793
1913
  docMain,
1794
1914
  docForValidation: initialDocForValidation,
1795
- derivedWorktree,
1915
+ derivedWorktree: effectiveDerivedWorktree,
1796
1916
  });
1797
1917
  const title = preFlightResult.title;
1798
1918
  // Note: docForValidation is returned but not used after pre-flight checks
1799
1919
  // The metadata transaction uses docForUpdate instead
1800
1920
  // 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;
1921
+ const worktreePath = effectiveWorktreePath;
1806
1922
  // WU-1943: Check if any checkpoints exist for this WU session
1807
1923
  // Warn (don't block) if no checkpoints - agent should have been checkpointing periodically
1808
1924
  try {
@@ -1816,9 +1932,16 @@ async function main() {
1816
1932
  catch {
1817
1933
  // Non-blocking: checkpoint check failure should not block wu:done
1818
1934
  }
1819
- await executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath });
1935
+ await executeGates({ id, args, isBranchOnly: effectiveBranchOnly, isDocsOnly, worktreePath });
1820
1936
  // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
1821
- printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR });
1937
+ printStateHUD({
1938
+ id,
1939
+ docMain,
1940
+ isBranchOnly: effectiveBranchOnly,
1941
+ isDocsOnly,
1942
+ derivedWorktree: effectiveDerivedWorktree,
1943
+ STAMPS_DIR,
1944
+ });
1822
1945
  // Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
1823
1946
  // This prevents partial completion states where merge succeeds but commit fails
1824
1947
  // Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
@@ -1855,7 +1978,7 @@ async function main() {
1855
1978
  validateStagedFiles,
1856
1979
  };
1857
1980
  try {
1858
- if (isBranchOnly) {
1981
+ if (effectiveBranchOnly) {
1859
1982
  // Branch-Only mode: merge first, then update metadata on main
1860
1983
  // NOTE: Branch-only still uses old rollback mechanism
1861
1984
  const branchOnlyContext = {
@@ -2073,7 +2196,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
2073
2196
  // Get files changed in this branch vs base
2074
2197
  const diff = await git.raw(['diff', '--name-only', baseBranch]);
2075
2198
  const changedFiles = diff.split('\n').filter(Boolean);
2076
- // Filter to docs: ai/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
2199
+ // Filter to docs: docs/04-operations/_frameworks/lumenflow/agent/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
2077
2200
  const docPatterns = [
2078
2201
  /^ai\/onboarding\//,
2079
2202
  /^docs\//,