@lumenflow/cli 2.2.1 → 2.3.1

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.
Files changed (119) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/release.test.js +28 -0
  10. package/dist/__tests__/safe-git.test.js +191 -0
  11. package/dist/__tests__/state-doctor.test.js +274 -0
  12. package/dist/__tests__/wu-done.test.js +36 -0
  13. package/dist/__tests__/wu-edit.test.js +119 -0
  14. package/dist/__tests__/wu-prep.test.js +108 -0
  15. package/dist/agent-issues-query.js +4 -3
  16. package/dist/agent-log-issue.js +25 -4
  17. package/dist/backlog-prune.js +5 -4
  18. package/dist/cli-entry-point.js +11 -1
  19. package/dist/doctor.js +368 -0
  20. package/dist/flow-bottlenecks.js +6 -5
  21. package/dist/flow-report.js +4 -3
  22. package/dist/gates.js +468 -116
  23. package/dist/guard-locked.js +4 -3
  24. package/dist/guard-worktree-commit.js +4 -3
  25. package/dist/init.js +508 -86
  26. package/dist/initiative-add-wu.js +4 -3
  27. package/dist/initiative-bulk-assign-wus.js +8 -5
  28. package/dist/initiative-create.js +73 -37
  29. package/dist/initiative-edit.js +37 -21
  30. package/dist/initiative-list.js +4 -3
  31. package/dist/initiative-plan.js +337 -0
  32. package/dist/initiative-status.js +4 -3
  33. package/dist/lane-health.js +377 -0
  34. package/dist/lane-suggest.js +382 -0
  35. package/dist/mem-checkpoint.js +2 -2
  36. package/dist/mem-cleanup.js +2 -2
  37. package/dist/mem-context.js +306 -0
  38. package/dist/mem-create.js +2 -2
  39. package/dist/mem-delete.js +293 -0
  40. package/dist/mem-inbox.js +2 -2
  41. package/dist/mem-index.js +211 -0
  42. package/dist/mem-init.js +1 -1
  43. package/dist/mem-profile.js +207 -0
  44. package/dist/mem-promote.js +254 -0
  45. package/dist/mem-ready.js +2 -2
  46. package/dist/mem-signal.js +2 -2
  47. package/dist/mem-start.js +2 -2
  48. package/dist/mem-summarize.js +2 -2
  49. package/dist/mem-triage.js +2 -2
  50. package/dist/merge-block.js +222 -0
  51. package/dist/metrics-cli.js +7 -4
  52. package/dist/metrics-snapshot.js +4 -3
  53. package/dist/orchestrate-initiative.js +10 -4
  54. package/dist/orchestrate-monitor.js +379 -31
  55. package/dist/signal-cleanup.js +296 -0
  56. package/dist/spawn-list.js +6 -5
  57. package/dist/state-bootstrap.js +5 -4
  58. package/dist/state-cleanup.js +360 -0
  59. package/dist/state-doctor-fix.js +196 -0
  60. package/dist/state-doctor.js +501 -0
  61. package/dist/validate-agent-skills.js +4 -3
  62. package/dist/validate-agent-sync.js +4 -3
  63. package/dist/validate-backlog-sync.js +7 -84
  64. package/dist/validate-skills-spec.js +4 -3
  65. package/dist/validate.js +7 -107
  66. package/dist/wu-block.js +3 -3
  67. package/dist/wu-claim.js +208 -98
  68. package/dist/wu-cleanup.js +5 -4
  69. package/dist/wu-create.js +71 -46
  70. package/dist/wu-delete.js +88 -60
  71. package/dist/wu-deps.js +6 -5
  72. package/dist/wu-done-check.js +34 -0
  73. package/dist/wu-done.js +60 -24
  74. package/dist/wu-edit.js +63 -28
  75. package/dist/wu-infer-lane.js +7 -6
  76. package/dist/wu-preflight.js +23 -81
  77. package/dist/wu-prep.js +125 -0
  78. package/dist/wu-prune.js +4 -3
  79. package/dist/wu-recover.js +88 -22
  80. package/dist/wu-repair.js +7 -6
  81. package/dist/wu-spawn.js +226 -270
  82. package/dist/wu-status.js +4 -3
  83. package/dist/wu-unblock.js +5 -5
  84. package/dist/wu-unlock-lane.js +4 -3
  85. package/dist/wu-validate.js +5 -4
  86. package/package.json +16 -7
  87. package/templates/core/.lumenflow/constraints.md.template +192 -0
  88. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  89. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  90. package/templates/core/AGENTS.md.template +60 -0
  91. package/templates/core/LUMENFLOW.md.template +255 -0
  92. package/templates/core/UPGRADING.md.template +121 -0
  93. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  94. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  95. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  96. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  97. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  98. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  99. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  100. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  101. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  102. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  103. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  104. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  105. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  106. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  107. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  108. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  109. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  110. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  111. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  112. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  113. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  114. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  115. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  116. package/templates/vendors/cline/.clinerules.template +53 -0
  117. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  118. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  119. package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
@@ -227,9 +227,10 @@ async function main() {
227
227
  `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
228
228
  }
229
229
  }
230
- // Guard main() for testability
231
- import { fileURLToPath } from 'node:url';
230
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
231
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
232
+ // path but import.meta.url resolves to the real path - they never match
232
233
  import { runCLI } from './cli-entry-point.js';
233
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
234
+ if (import.meta.main) {
234
235
  runCLI(main);
235
236
  }
@@ -21,14 +21,16 @@ import { parse as parseYaml } from 'yaml';
21
21
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
22
22
  import { die } from '@lumenflow/core/dist/error-handler.js';
23
23
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
24
+ import { createWuPaths } from '@lumenflow/core/dist/wu-paths.js';
24
25
  /** Log prefix for console output */
25
26
  const LOG_PREFIX = '[initiative:bulk-assign]';
26
27
  /** Default lane bucket configuration path */
27
28
  const DEFAULT_CONFIG_PATH = 'tools/config/initiative-lane-buckets.yaml';
29
+ const wuPaths = createWuPaths();
28
30
  /** WU directory relative to repo root */
29
- const WU_DIR = 'docs/04-operations/tasks/wu';
31
+ const WU_DIR = wuPaths.WU_DIR();
30
32
  /** Initiative directory relative to repo root */
31
- const INIT_DIR = 'docs/04-operations/tasks/initiatives';
33
+ const INIT_DIR = wuPaths.INITIATIVES_DIR();
32
34
  /** Environment variable required for apply mode */
33
35
  const ADMIN_ENV_VAR = 'LUMENFLOW_ADMIN';
34
36
  /** Micro-worktree operation name */
@@ -306,9 +308,10 @@ async function main() {
306
308
  });
307
309
  console.log(`${LOG_PREFIX} ✅ Successfully applied ${changes.length} changes`);
308
310
  }
309
- // Guard main() for testability (WU-1366)
310
- import { fileURLToPath } from 'node:url';
311
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
311
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
312
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
313
+ // path but import.meta.url resolves to the real path - they never match
314
+ if (import.meta.main) {
312
315
  main().catch((err) => {
313
316
  die(`Bulk assign failed: ${err.message}`);
314
317
  });
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable security/detect-non-literal-fs-filename */
3
+ /* eslint-disable no-console -- CLI tool requires console output */
4
+ /* eslint-disable @typescript-eslint/explicit-function-return-type -- CLI tool uses dynamic YAML types */
3
5
  /**
4
6
  * Initiative Create Helper (WU-1247, WU-1439)
5
7
  *
@@ -34,6 +36,8 @@ import { stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
34
36
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
35
37
  import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
36
38
  import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_DEFAULTS, } from '@lumenflow/initiatives/dist/initiative-constants.js';
39
+ // WU-1211: Import initiative validation for completeness warnings
40
+ import { validateInitiativeCompleteness } from '@lumenflow/initiatives/dist/initiative-validation.js';
37
41
  import { FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
38
42
  import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
39
43
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
@@ -124,46 +128,78 @@ async function main() {
124
128
  checkInitiativeExists(args.id);
125
129
  await ensureOnMain(getGitForCwd());
126
130
  // Transaction: micro-worktree isolation (WU-1439)
131
+ // WU-1255: Set LUMENFLOW_WU_TOOL to allow pre-push hook bypass for micro-worktree pushes
132
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
133
+ process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
127
134
  try {
128
- await withMicroWorktree({
129
- operation: OPERATION_NAME,
130
- id: args.id,
131
- logPrefix: LOG_PREFIX,
132
- execute: async ({ worktreePath }) => {
133
- // Create Initiative YAML in micro-worktree
134
- const initPath = createInitiativeYamlInWorktree(worktreePath, args.id, args.slug, args.title, {
135
- priority: args.priority,
136
- owner: args.owner,
137
- targetDate: args.targetDate,
138
- });
139
- // Return commit message and files to commit
140
- return {
141
- commitMessage: INIT_COMMIT_FORMATS.CREATE(args.id, args.title),
142
- files: [initPath],
143
- };
144
- },
145
- });
146
- console.log(`\n${LOG_PREFIX} ✅ Transaction complete!`);
147
- console.log(`\nInitiative Created:`);
148
- console.log(` ID: ${args.id}`);
149
- console.log(` Slug: ${args.slug}`);
150
- console.log(` Title: ${args.title}`);
151
- console.log(` Status: ${INIT_DEFAULTS.STATUS}`);
152
- console.log(` File: ${INIT_PATHS.INITIATIVE(args.id)}`);
153
- console.log(`\nNext steps:`);
154
- console.log(` 1. Edit ${args.id}.yaml to add description, phases, and success_metrics`);
155
- console.log(` 2. Link WUs to this initiative: pnpm wu:create --initiative ${args.id} ...`);
156
- console.log(` 3. View status: pnpm initiative:status ${args.id}`);
135
+ try {
136
+ await withMicroWorktree({
137
+ operation: OPERATION_NAME,
138
+ id: args.id,
139
+ logPrefix: LOG_PREFIX,
140
+ execute: async ({ worktreePath }) => {
141
+ // Create Initiative YAML in micro-worktree
142
+ const initPath = createInitiativeYamlInWorktree(worktreePath, args.id, args.slug, args.title, {
143
+ priority: args.priority,
144
+ owner: args.owner,
145
+ targetDate: args.targetDate,
146
+ });
147
+ // Return commit message and files to commit
148
+ return {
149
+ commitMessage: INIT_COMMIT_FORMATS.CREATE(args.id, args.title),
150
+ files: [initPath],
151
+ };
152
+ },
153
+ });
154
+ console.log(`\n${LOG_PREFIX} ✅ Transaction complete!`);
155
+ console.log(`\nInitiative Created:`);
156
+ console.log(` ID: ${args.id}`);
157
+ console.log(` Slug: ${args.slug}`);
158
+ console.log(` Title: ${args.title}`);
159
+ console.log(` Status: ${INIT_DEFAULTS.STATUS}`);
160
+ console.log(` File: ${INIT_PATHS.INITIATIVE(args.id)}`);
161
+ // WU-1211: Check completeness and emit warnings
162
+ const initContent = {
163
+ id: args.id,
164
+ slug: args.slug,
165
+ title: args.title,
166
+ description: '',
167
+ status: INIT_DEFAULTS.STATUS,
168
+ phases: [],
169
+ success_metrics: [],
170
+ };
171
+ const completenessResult = validateInitiativeCompleteness(initContent);
172
+ if (completenessResult.warnings.length > 0) {
173
+ console.log(`\n${LOG_PREFIX} ⚠️ Incomplete initiative - missing recommended fields:`);
174
+ for (const warning of completenessResult.warnings) {
175
+ console.log(` - ${warning}`);
176
+ }
177
+ }
178
+ console.log(`\nNext steps:`);
179
+ console.log(` 1. Edit ${args.id}.yaml to add description, phases, and success_metrics`);
180
+ console.log(` 2. Link WUs to this initiative: pnpm wu:create --initiative ${args.id} ...`);
181
+ console.log(` 3. View status: pnpm initiative:status ${args.id}`);
182
+ }
183
+ catch (error) {
184
+ die(`Transaction failed: ${error.message}\n\n` +
185
+ `Micro-worktree cleanup was attempted automatically.\n` +
186
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
187
+ }
157
188
  }
158
- catch (error) {
159
- die(`Transaction failed: ${error.message}\n\n` +
160
- `Micro-worktree cleanup was attempted automatically.\n` +
161
- `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
189
+ finally {
190
+ // WU-1255: Restore LUMENFLOW_WU_TOOL to previous value
191
+ if (previousWuTool === undefined) {
192
+ delete process.env.LUMENFLOW_WU_TOOL;
193
+ }
194
+ else {
195
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
196
+ }
162
197
  }
163
198
  }
164
- // Guard main() for testability
165
- import { fileURLToPath } from 'node:url';
199
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
200
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
201
+ // path but import.meta.url resolves to the real path - they never match
166
202
  import { runCLI } from './cli-entry-point.js';
167
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
168
- runCLI(main);
203
+ if (import.meta.main) {
204
+ void runCLI(main);
169
205
  }
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /* eslint-disable @typescript-eslint/explicit-function-return-type -- CLI tool uses dynamic YAML types */
2
4
  /**
3
5
  * Initiative Edit Helper
4
6
  *
@@ -29,7 +31,7 @@
29
31
  * pnpm initiative:edit --id INIT-001 --notes "Phase 2 started"
30
32
  *
31
33
  * Part of WU-1451: Add initiative:edit command for updating initiative status and blockers
32
- * @see {@link tools/lib/micro-worktree.mjs} - Shared micro-worktree logic
34
+ * @see {@link packages/@lumenflow/cli/src/lib/micro-worktree.ts} - Shared micro-worktree logic
33
35
  */
34
36
  import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
35
37
  import { die } from '@lumenflow/core/dist/error-handler.js';
@@ -413,26 +415,40 @@ async function main() {
413
415
  await ensureCleanWorkingTree();
414
416
  await ensureMainUpToDate(getGitForCwd(), 'initiative:edit');
415
417
  console.log(`${PREFIX} Applying edits via micro-worktree...`);
416
- await withMicroWorktree({
417
- operation: MICRO_WORKTREE_OPERATIONS.INITIATIVE_EDIT,
418
- id: id,
419
- logPrefix: PREFIX,
420
- pushOnly: true, // WU-1435: Push directly to origin/main without touching local main
421
- execute: async ({ worktreePath }) => {
422
- // Write updated Initiative to micro-worktree
423
- const initPath = join(worktreePath, INIT_PATHS.INITIATIVE(id));
424
- const yamlContent = stringifyYAML(updatedInit);
425
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes init files
426
- writeFileSync(initPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
427
- console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
428
- return {
429
- commitMessage: INIT_COMMIT_FORMATS.EDIT(id),
430
- files: [INIT_PATHS.INITIATIVE(id)],
431
- };
432
- },
433
- });
434
- console.log(`${PREFIX} Successfully edited ${id}`);
435
- console.log(`${PREFIX} Changes pushed to origin/main`);
418
+ // WU-1255: Set LUMENFLOW_WU_TOOL to allow pre-push hook bypass for micro-worktree pushes
419
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
420
+ process.env.LUMENFLOW_WU_TOOL = MICRO_WORKTREE_OPERATIONS.INITIATIVE_EDIT;
421
+ try {
422
+ await withMicroWorktree({
423
+ operation: MICRO_WORKTREE_OPERATIONS.INITIATIVE_EDIT,
424
+ id: id,
425
+ logPrefix: PREFIX,
426
+ pushOnly: true, // WU-1435: Push directly to origin/main without touching local main
427
+ execute: async ({ worktreePath }) => {
428
+ // Write updated Initiative to micro-worktree
429
+ const initPath = join(worktreePath, INIT_PATHS.INITIATIVE(id));
430
+ const yamlContent = stringifyYAML(updatedInit);
431
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes init files
432
+ writeFileSync(initPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
433
+ console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
434
+ return {
435
+ commitMessage: INIT_COMMIT_FORMATS.EDIT(id),
436
+ files: [INIT_PATHS.INITIATIVE(id)],
437
+ };
438
+ },
439
+ });
440
+ console.log(`${PREFIX} Successfully edited ${id}`);
441
+ console.log(`${PREFIX} Changes pushed to origin/main`);
442
+ }
443
+ finally {
444
+ // WU-1255: Restore LUMENFLOW_WU_TOOL to previous value
445
+ if (previousWuTool === undefined) {
446
+ delete process.env.LUMENFLOW_WU_TOOL;
447
+ }
448
+ else {
449
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
450
+ }
451
+ }
436
452
  }
437
453
  main().catch((err) => {
438
454
  console.error(`${PREFIX} ${err.message}`);
@@ -94,9 +94,10 @@ async function main() {
94
94
  break;
95
95
  }
96
96
  }
97
- // Guard main() for testability (WU-1366)
98
- import { fileURLToPath } from 'node:url';
97
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
98
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
99
+ // path but import.meta.url resolves to the real path - they never match
99
100
  import { runCLI } from './cli-entry-point.js';
100
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
101
+ if (import.meta.main) {
101
102
  runCLI(main);
102
103
  }
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable security/detect-non-literal-fs-filename */
3
+ /**
4
+ * Initiative Plan Command (WU-1105, renamed in WU-1193)
5
+ *
6
+ * Links plan files to initiatives by setting the `related_plan` field
7
+ * in the initiative YAML.
8
+ *
9
+ * Usage:
10
+ * pnpm initiative:plan --initiative INIT-001 --plan docs/04-operations/plans/my-plan.md
11
+ * pnpm initiative:plan --initiative INIT-001 --create # Create new plan template
12
+ *
13
+ * Features:
14
+ * - Validates initiative exists before modifying
15
+ * - Formats plan path as lumenflow:// URI
16
+ * - Idempotent: no error if same plan already linked
17
+ * - Warns if replacing existing plan link
18
+ * - Can create plan templates with --create
19
+ *
20
+ * Context: WU-1105 (INIT-003 Phase 3a), renamed from init:plan in WU-1193
21
+ */
22
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
23
+ import { die } from '@lumenflow/core/dist/error-handler.js';
24
+ import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs';
25
+ import { join, basename } from 'node:path';
26
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
27
+ import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
28
+ import { INIT_PATTERNS } from '@lumenflow/initiatives/dist/initiative-constants.js';
29
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
30
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
31
+ import { readInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
32
+ import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
33
+ import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
34
+ /** Log prefix for console output */
35
+ export const LOG_PREFIX = CORE_LOG_PREFIX.INITIATIVE_PLAN;
36
+ /** Micro-worktree operation name */
37
+ const OPERATION_NAME = 'initiative-plan';
38
+ /** Standard plans directory relative to repo root */
39
+ const PLANS_DIR = 'docs/04-operations/plans';
40
+ /** LumenFlow URI scheme for plan references */
41
+ const PLAN_URI_SCHEME = 'lumenflow://plans/';
42
+ /**
43
+ * Custom option for plan file path
44
+ */
45
+ const PLAN_OPTION = {
46
+ name: 'plan',
47
+ flags: '--plan <path>',
48
+ description: 'Path to plan file (markdown)',
49
+ };
50
+ /**
51
+ * Custom option for creating new plan template
52
+ */
53
+ const CREATE_OPTION = {
54
+ name: 'create',
55
+ flags: '--create',
56
+ description: 'Create a new plan template instead of linking existing file',
57
+ };
58
+ /**
59
+ * Validate Initiative ID format
60
+ * @param id - Initiative ID to validate
61
+ * @throws Error if format is invalid
62
+ */
63
+ export function validateInitIdFormat(id) {
64
+ if (!INIT_PATTERNS.INIT_ID.test(id)) {
65
+ die(`Invalid Initiative ID format: "${id}"\n\n` +
66
+ `Expected format: INIT-<number> or INIT-NAME (e.g., INIT-001, INIT-TOOLING)`);
67
+ }
68
+ }
69
+ /**
70
+ * Validate plan file path
71
+ * @param planPath - Path to plan file
72
+ * @throws Error if path is invalid or file doesn't exist
73
+ */
74
+ export function validatePlanPath(planPath) {
75
+ if (!planPath.endsWith('.md')) {
76
+ die(`Invalid plan file format: "${planPath}"\n\nPlan files must be markdown (.md)`);
77
+ }
78
+ if (!existsSync(planPath)) {
79
+ die(`Plan file not found: "${planPath}"\n\nUse --create to create a new plan template`);
80
+ }
81
+ }
82
+ /**
83
+ * Format plan path as lumenflow:// URI
84
+ *
85
+ * Extracts the filename (and any subdirectory within plans/) and creates
86
+ * a standardized URI for the plan reference.
87
+ *
88
+ * @param planPath - Path to plan file (can be relative or absolute)
89
+ * @returns lumenflow://plans/<filename> URI
90
+ */
91
+ export function formatPlanUri(planPath) {
92
+ // Try to extract path relative to plans directory
93
+ const plansMarker = '/plans/';
94
+ const plansIndex = planPath.indexOf(plansMarker);
95
+ if (plansIndex !== -1) {
96
+ // Extract everything after /plans/
97
+ const relativePath = planPath.substring(plansIndex + plansMarker.length);
98
+ return `${PLAN_URI_SCHEME}${relativePath}`;
99
+ }
100
+ // Fallback: just use the filename
101
+ const filename = basename(planPath);
102
+ return `${PLAN_URI_SCHEME}${filename}`;
103
+ }
104
+ /**
105
+ * Check if initiative exists and return the document
106
+ * @param initId - Initiative ID to check
107
+ * @returns Initiative document
108
+ * @throws Error if initiative not found
109
+ */
110
+ export function checkInitiativeExists(initId) {
111
+ const initPath = INIT_PATHS.INITIATIVE(initId);
112
+ if (!existsSync(initPath)) {
113
+ die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
114
+ }
115
+ return readInitiative(initPath, initId);
116
+ }
117
+ /**
118
+ * Update initiative with plan reference in micro-worktree
119
+ *
120
+ * Uses raw YAML parsing to preserve unknown fields like related_plan
121
+ * that are not in the strict initiative schema.
122
+ *
123
+ * @param worktreePath - Path to micro-worktree
124
+ * @param initId - Initiative ID
125
+ * @param planUri - Plan URI to set
126
+ * @returns True if changes were made, false if already linked
127
+ */
128
+ export function updateInitiativeWithPlan(worktreePath, initId, planUri) {
129
+ const initRelPath = INIT_PATHS.INITIATIVE(initId);
130
+ const initAbsPath = join(worktreePath, initRelPath);
131
+ // Read raw YAML to preserve unknown fields like related_plan
132
+ // (readInitiative strips them via zod schema validation)
133
+ const rawText = readFileSync(initAbsPath, { encoding: 'utf-8' });
134
+ const doc = parseYAML(rawText);
135
+ // Validate ID matches
136
+ if (doc.id !== initId) {
137
+ die(`Initiative YAML id mismatch. Expected ${initId}, found ${doc.id}`);
138
+ }
139
+ // Check for existing plan link
140
+ const existingPlan = doc.related_plan;
141
+ if (existingPlan === planUri) {
142
+ // Already linked to same plan - idempotent
143
+ return false;
144
+ }
145
+ if (existingPlan && existingPlan !== planUri) {
146
+ // Different plan already linked - warn but proceed
147
+ console.warn(`${LOG_PREFIX} Replacing existing related_plan: ${existingPlan} -> ${planUri}`);
148
+ }
149
+ // Update related_plan field
150
+ doc.related_plan = planUri;
151
+ const out = stringifyYAML(doc);
152
+ writeFileSync(initAbsPath, out, { encoding: 'utf-8' });
153
+ console.log(`${LOG_PREFIX} Updated ${initId} with related_plan: ${planUri}`);
154
+ return true;
155
+ }
156
+ /**
157
+ * Create a plan template file
158
+ *
159
+ * @param worktreePath - Path to repo root or worktree
160
+ * @param initId - Initiative ID
161
+ * @param title - Initiative title
162
+ * @returns Path to created file
163
+ * @throws Error if file already exists
164
+ */
165
+ export function createPlanTemplate(worktreePath, initId, title) {
166
+ const slug = title
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9]+/g, '-')
169
+ .replace(/^-|-$/g, '')
170
+ .substring(0, 30);
171
+ const filename = `${initId}-${slug}.md`;
172
+ const plansDir = join(worktreePath, PLANS_DIR);
173
+ const planPath = join(plansDir, filename);
174
+ if (existsSync(planPath)) {
175
+ die(`Plan file already exists: ${planPath}\n\nUse --plan to link an existing file`);
176
+ }
177
+ // Ensure plans directory exists
178
+ if (!existsSync(plansDir)) {
179
+ mkdirSync(plansDir, { recursive: true });
180
+ }
181
+ const template = `# ${initId} Plan - ${title}
182
+
183
+ ## Goal
184
+
185
+ <!-- What is the primary objective of this initiative? -->
186
+
187
+ ## Scope
188
+
189
+ <!-- What is in scope and out of scope? -->
190
+
191
+ ## Approach
192
+
193
+ <!-- How will you achieve the goal? Key phases or milestones? -->
194
+
195
+ ## Success Criteria
196
+
197
+ <!-- How will you know when this is complete? Measurable outcomes? -->
198
+
199
+ ## Risks
200
+
201
+ <!-- What could go wrong? How will you mitigate? -->
202
+
203
+ ## References
204
+
205
+ - Initiative: ${initId}
206
+ - Created: ${new Date().toISOString().split('T')[0]}
207
+ `;
208
+ writeFileSync(planPath, template, { encoding: 'utf-8' });
209
+ console.log(`${LOG_PREFIX} Created plan template: ${planPath}`);
210
+ return planPath;
211
+ }
212
+ /**
213
+ * Generate commit message for plan link operation
214
+ */
215
+ export function getCommitMessage(initId, planUri) {
216
+ const filename = planUri.replace(PLAN_URI_SCHEME, '');
217
+ return `docs: link plan ${filename} to ${initId.toLowerCase()}`;
218
+ }
219
+ async function main() {
220
+ const args = createWUParser({
221
+ name: 'init-plan',
222
+ description: 'Link a plan file to an initiative',
223
+ options: [WU_OPTIONS.initiative, PLAN_OPTION, CREATE_OPTION],
224
+ required: ['initiative'],
225
+ allowPositionalId: false,
226
+ });
227
+ const initId = args.initiative;
228
+ const planPath = args.plan;
229
+ const shouldCreate = args.create;
230
+ // Validate inputs
231
+ validateInitIdFormat(initId);
232
+ // Check initiative exists first (before any mutations)
233
+ const initDoc = checkInitiativeExists(initId);
234
+ const initTitle = initDoc.title;
235
+ // Determine plan path and URI
236
+ let targetPlanPath;
237
+ let planUri;
238
+ if (shouldCreate) {
239
+ // Create mode - will create template and link it
240
+ console.log(`${LOG_PREFIX} Creating plan template for ${initId}...`);
241
+ // Ensure on main for micro-worktree operations
242
+ await ensureOnMain(getGitForCwd());
243
+ try {
244
+ await withMicroWorktree({
245
+ operation: OPERATION_NAME,
246
+ id: initId,
247
+ logPrefix: LOG_PREFIX,
248
+ pushOnly: true,
249
+ execute: async ({ worktreePath }) => {
250
+ // Create plan template
251
+ targetPlanPath = createPlanTemplate(worktreePath, initId, initTitle);
252
+ planUri = formatPlanUri(targetPlanPath);
253
+ // Update initiative with plan link
254
+ updateInitiativeWithPlan(worktreePath, initId, planUri);
255
+ // Return files to commit
256
+ const planRelPath = targetPlanPath.replace(worktreePath + '/', '');
257
+ return {
258
+ commitMessage: getCommitMessage(initId, planUri),
259
+ files: [planRelPath, INIT_PATHS.INITIATIVE(initId)],
260
+ };
261
+ },
262
+ });
263
+ console.log(`\n${LOG_PREFIX} Transaction complete!`);
264
+ console.log(`\nPlan Linked:`);
265
+ console.log(` Initiative: ${initId}`);
266
+ console.log(` Plan URI: ${planUri}`);
267
+ console.log(` File: ${targetPlanPath}`);
268
+ console.log(`\nNext steps:`);
269
+ console.log(` 1. Edit the plan file with your goals and approach`);
270
+ console.log(` 2. View initiative: pnpm initiative:status ${initId}`);
271
+ }
272
+ catch (error) {
273
+ die(`Transaction failed: ${error.message}\n\n` +
274
+ `Micro-worktree cleanup was attempted automatically.\n` +
275
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
276
+ }
277
+ }
278
+ else if (planPath) {
279
+ // Link existing file mode
280
+ validatePlanPath(planPath);
281
+ planUri = formatPlanUri(planPath);
282
+ console.log(`${LOG_PREFIX} Linking plan to ${initId}...`);
283
+ // Check for idempotent case before micro-worktree
284
+ const existingPlan = initDoc.related_plan;
285
+ if (existingPlan === planUri) {
286
+ console.log(`${LOG_PREFIX} Plan already linked (idempotent - no changes needed)`);
287
+ console.log(`\n${LOG_PREFIX} ${initId} already has related_plan: ${planUri}`);
288
+ return;
289
+ }
290
+ // Ensure on main for micro-worktree operations
291
+ await ensureOnMain(getGitForCwd());
292
+ try {
293
+ await withMicroWorktree({
294
+ operation: OPERATION_NAME,
295
+ id: initId,
296
+ logPrefix: LOG_PREFIX,
297
+ pushOnly: true,
298
+ execute: async ({ worktreePath }) => {
299
+ // Update initiative with plan link
300
+ const changed = updateInitiativeWithPlan(worktreePath, initId, planUri);
301
+ if (!changed) {
302
+ console.log(`${LOG_PREFIX} No changes detected (concurrent link operation)`);
303
+ }
304
+ return {
305
+ commitMessage: getCommitMessage(initId, planUri),
306
+ files: [INIT_PATHS.INITIATIVE(initId)],
307
+ };
308
+ },
309
+ });
310
+ console.log(`\n${LOG_PREFIX} Transaction complete!`);
311
+ console.log(`\nPlan Linked:`);
312
+ console.log(` Initiative: ${initId}`);
313
+ console.log(` Plan URI: ${planUri}`);
314
+ console.log(` File: ${planPath}`);
315
+ console.log(`\nNext steps:`);
316
+ console.log(` - View initiative: pnpm initiative:status ${initId}`);
317
+ }
318
+ catch (error) {
319
+ die(`Transaction failed: ${error.message}\n\n` +
320
+ `Micro-worktree cleanup was attempted automatically.\n` +
321
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
322
+ }
323
+ }
324
+ else {
325
+ die('Either --plan or --create is required\n\n' +
326
+ 'Usage:\n' +
327
+ ' pnpm init:plan --initiative INIT-001 --plan docs/04-operations/plans/my-plan.md\n' +
328
+ ' pnpm init:plan --initiative INIT-001 --create');
329
+ }
330
+ }
331
+ // Guard main() for testability - use import.meta.main (WU-1071)
332
+ import { runCLI } from './cli-entry-point.js';
333
+ if (import.meta.main) {
334
+ runCLI(main);
335
+ }
336
+ // Export for testing
337
+ export { main };
@@ -214,9 +214,10 @@ async function main() {
214
214
  break;
215
215
  }
216
216
  }
217
- // Guard main() for testability (WU-1366)
218
- import { fileURLToPath } from 'node:url';
217
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
218
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
219
+ // path but import.meta.url resolves to the real path - they never match
219
220
  import { runCLI } from './cli-entry-point.js';
220
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
221
+ if (import.meta.main) {
221
222
  runCLI(main);
222
223
  }