@lumenflow/cli 2.4.0 → 2.5.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 (147) hide show
  1. package/README.md +11 -8
  2. package/dist/__tests__/init-config-lanes.test.js +131 -0
  3. package/dist/__tests__/init-docs-structure.test.js +119 -0
  4. package/dist/__tests__/init-lane-inference.test.js +125 -0
  5. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  6. package/dist/__tests__/init-quick-ref.test.js +145 -0
  7. package/dist/__tests__/init-scripts.test.js +207 -0
  8. package/dist/__tests__/init-template-portability.test.js +97 -0
  9. package/dist/__tests__/init.test.js +7 -2
  10. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  11. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  12. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  13. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  14. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  15. package/dist/__tests__/plan-create.test.js +126 -0
  16. package/dist/__tests__/plan-edit.test.js +157 -0
  17. package/dist/__tests__/plan-link.test.js +239 -0
  18. package/dist/__tests__/plan-promote.test.js +181 -0
  19. package/dist/__tests__/templates-sync.test.js +219 -0
  20. package/dist/__tests__/wu-create-strict.test.js +118 -0
  21. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  22. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  23. package/dist/flow-bottlenecks.js +4 -2
  24. package/dist/gates.js +22 -0
  25. package/dist/init.js +670 -87
  26. package/dist/initiative-add-wu.js +112 -16
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/onboarding-smoke-test.js +400 -0
  29. package/dist/orchestrate-init-status.js +37 -9
  30. package/dist/orchestrate-initiative.js +10 -4
  31. package/dist/plan-create.js +199 -0
  32. package/dist/plan-edit.js +235 -0
  33. package/dist/plan-link.js +233 -0
  34. package/dist/plan-promote.js +231 -0
  35. package/dist/sync-templates.js +137 -5
  36. package/dist/wu-block.js +16 -5
  37. package/dist/wu-claim.js +15 -9
  38. package/dist/wu-create.js +50 -2
  39. package/dist/wu-deps.js +3 -1
  40. package/dist/wu-done.js +14 -5
  41. package/dist/wu-edit.js +35 -0
  42. package/dist/wu-prep.js +131 -8
  43. package/dist/wu-spawn.js +14 -1
  44. package/dist/wu-unblock.js +34 -2
  45. package/dist/wu-validate.js +25 -17
  46. package/package.json +11 -7
  47. package/templates/core/.lumenflow/constraints.md.template +61 -3
  48. package/templates/core/AGENTS.md.template +2 -2
  49. package/templates/core/LUMENFLOW.md.template +85 -23
  50. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
  51. package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
  52. package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
  53. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
  54. package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
  55. package/templates/core/ai/onboarding/release-process.md.template +8 -2
  56. package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
  57. package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
  58. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
  59. package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
  60. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
  61. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
  62. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
  63. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
  64. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
  65. package/dist/__tests__/init-plan.test.js +0 -340
  66. package/dist/agent-issues-query.d.ts +0 -16
  67. package/dist/agent-log-issue.d.ts +0 -10
  68. package/dist/agent-session-end.d.ts +0 -10
  69. package/dist/agent-session.d.ts +0 -10
  70. package/dist/backlog-prune.d.ts +0 -84
  71. package/dist/cli-entry-point.d.ts +0 -8
  72. package/dist/deps-add.d.ts +0 -91
  73. package/dist/deps-remove.d.ts +0 -17
  74. package/dist/docs-sync.d.ts +0 -50
  75. package/dist/file-delete.d.ts +0 -84
  76. package/dist/file-edit.d.ts +0 -82
  77. package/dist/file-read.d.ts +0 -92
  78. package/dist/file-write.d.ts +0 -90
  79. package/dist/flow-bottlenecks.d.ts +0 -16
  80. package/dist/flow-report.d.ts +0 -16
  81. package/dist/gates.d.ts +0 -94
  82. package/dist/git-branch.d.ts +0 -65
  83. package/dist/git-diff.d.ts +0 -58
  84. package/dist/git-log.d.ts +0 -69
  85. package/dist/git-status.d.ts +0 -58
  86. package/dist/guard-locked.d.ts +0 -62
  87. package/dist/guard-main-branch.d.ts +0 -50
  88. package/dist/guard-worktree-commit.d.ts +0 -59
  89. package/dist/index.d.ts +0 -10
  90. package/dist/init-plan.d.ts +0 -80
  91. package/dist/init-plan.js +0 -337
  92. package/dist/init.d.ts +0 -46
  93. package/dist/initiative-add-wu.d.ts +0 -22
  94. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  95. package/dist/initiative-create.d.ts +0 -28
  96. package/dist/initiative-edit.d.ts +0 -34
  97. package/dist/initiative-list.d.ts +0 -12
  98. package/dist/initiative-status.d.ts +0 -11
  99. package/dist/lumenflow-upgrade.d.ts +0 -103
  100. package/dist/mem-checkpoint.d.ts +0 -16
  101. package/dist/mem-cleanup.d.ts +0 -29
  102. package/dist/mem-create.d.ts +0 -17
  103. package/dist/mem-export.d.ts +0 -10
  104. package/dist/mem-inbox.d.ts +0 -35
  105. package/dist/mem-init.d.ts +0 -15
  106. package/dist/mem-ready.d.ts +0 -16
  107. package/dist/mem-signal.d.ts +0 -16
  108. package/dist/mem-start.d.ts +0 -16
  109. package/dist/mem-summarize.d.ts +0 -22
  110. package/dist/mem-triage.d.ts +0 -22
  111. package/dist/metrics-cli.d.ts +0 -90
  112. package/dist/metrics-snapshot.d.ts +0 -18
  113. package/dist/orchestrate-init-status.d.ts +0 -11
  114. package/dist/orchestrate-initiative.d.ts +0 -12
  115. package/dist/orchestrate-monitor.d.ts +0 -11
  116. package/dist/release.d.ts +0 -117
  117. package/dist/rotate-progress.d.ts +0 -48
  118. package/dist/session-coordinator.d.ts +0 -74
  119. package/dist/spawn-list.d.ts +0 -16
  120. package/dist/state-bootstrap.d.ts +0 -92
  121. package/dist/sync-templates.d.ts +0 -52
  122. package/dist/trace-gen.d.ts +0 -84
  123. package/dist/validate-agent-skills.d.ts +0 -50
  124. package/dist/validate-agent-sync.d.ts +0 -36
  125. package/dist/validate-backlog-sync.d.ts +0 -37
  126. package/dist/validate-skills-spec.d.ts +0 -40
  127. package/dist/validate.d.ts +0 -60
  128. package/dist/wu-block.d.ts +0 -16
  129. package/dist/wu-claim.d.ts +0 -74
  130. package/dist/wu-cleanup.d.ts +0 -35
  131. package/dist/wu-create.d.ts +0 -69
  132. package/dist/wu-delete.d.ts +0 -21
  133. package/dist/wu-deps.d.ts +0 -13
  134. package/dist/wu-done.d.ts +0 -225
  135. package/dist/wu-edit.d.ts +0 -63
  136. package/dist/wu-infer-lane.d.ts +0 -17
  137. package/dist/wu-preflight.d.ts +0 -47
  138. package/dist/wu-prune.d.ts +0 -16
  139. package/dist/wu-recover.d.ts +0 -37
  140. package/dist/wu-release.d.ts +0 -19
  141. package/dist/wu-repair.d.ts +0 -60
  142. package/dist/wu-spawn-completion.d.ts +0 -10
  143. package/dist/wu-spawn.d.ts +0 -192
  144. package/dist/wu-status.d.ts +0 -25
  145. package/dist/wu-unblock.d.ts +0 -16
  146. package/dist/wu-unlock-lane.d.ts +0 -19
  147. package/dist/wu-validate.d.ts +0 -16
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /* eslint-disable security/detect-non-literal-fs-filename */
4
+ /**
5
+ * Plan Create Command (WU-1313)
6
+ *
7
+ * Creates plan files in the repo-native plansDir (not LUMENFLOW_HOME).
8
+ * Plans can be linked to WUs (via spec_refs) or initiatives (via related_plan).
9
+ *
10
+ * Usage:
11
+ * pnpm plan:create --id WU-1313 --title "Feature plan"
12
+ * pnpm plan:create --id INIT-001 --title "Initiative plan"
13
+ *
14
+ * Features:
15
+ * - Creates plan in repo directories.plansDir
16
+ * - Supports both WU-XXX and INIT-XXX IDs
17
+ * - Uses micro-worktree isolation for atomic commits
18
+ * - Idempotent: fails if plan already exists (no overwrite)
19
+ *
20
+ * Context: WU-1313 (INIT-013 Plan Tooling)
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 } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
27
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
28
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
29
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
30
+ import { todayISO } from '@lumenflow/core/dist/date-utils.js';
31
+ import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
32
+ /** Log prefix for console output */
33
+ export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_CREATE ?? '[plan:create]';
34
+ /** Micro-worktree operation name */
35
+ const OPERATION_NAME = 'plan-create';
36
+ /** LumenFlow URI scheme for plan references */
37
+ const PLAN_URI_SCHEME = 'lumenflow://plans/';
38
+ /** WU ID pattern */
39
+ const WU_ID_PATTERN = /^WU-\d+$/;
40
+ /** Initiative ID pattern */
41
+ const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
42
+ /**
43
+ * Validate that the ID is a valid WU or Initiative ID
44
+ *
45
+ * @param id - ID to validate (WU-XXX or INIT-XXX)
46
+ * @throws Error if ID format is invalid
47
+ */
48
+ export function validatePlanId(id) {
49
+ if (!id) {
50
+ die(`ID is required\n\nExpected format: WU-XXX or INIT-XXX`);
51
+ }
52
+ const isWU = WU_ID_PATTERN.test(id);
53
+ const isInit = INIT_ID_PATTERN.test(id);
54
+ if (!isWU && !isInit) {
55
+ die(`Invalid ID format: "${id}"\n\n` +
56
+ `Expected format:\n` +
57
+ ` - WU ID: WU-<number> (e.g., WU-1313)\n` +
58
+ ` - Initiative ID: INIT-<alphanumeric> (e.g., INIT-001, INIT-TOOLING)`);
59
+ }
60
+ }
61
+ /**
62
+ * Get the lumenflow:// URI for a plan
63
+ *
64
+ * @param id - WU or Initiative ID
65
+ * @returns lumenflow://plans/{id}-plan.md URI
66
+ */
67
+ export function getPlanUri(id) {
68
+ return `${PLAN_URI_SCHEME}${id}-plan.md`;
69
+ }
70
+ /**
71
+ * Create a plan file in the repo plansDir
72
+ *
73
+ * @param worktreePath - Path to repo root or worktree
74
+ * @param id - WU or Initiative ID
75
+ * @param title - Plan title
76
+ * @returns Path to created file
77
+ * @throws Error if file already exists
78
+ */
79
+ export function createPlan(worktreePath, id, title) {
80
+ const plansDir = join(worktreePath, WU_PATHS.PLANS_DIR());
81
+ const planPath = join(plansDir, `${id}-plan.md`);
82
+ if (existsSync(planPath)) {
83
+ die(`Plan file already exists: ${planPath}\n\n` +
84
+ `Options:\n` +
85
+ ` 1. Edit the existing plan: pnpm plan:edit --id ${id}\n` +
86
+ ` 2. Delete and recreate (not recommended)\n` +
87
+ ` 3. Use plan:link to link existing plan to a WU/initiative`);
88
+ }
89
+ // Ensure plans directory exists
90
+ if (!existsSync(plansDir)) {
91
+ mkdirSync(plansDir, { recursive: true });
92
+ }
93
+ const today = todayISO();
94
+ const template = `# ${id} Plan - ${title}
95
+
96
+ Created: ${today}
97
+
98
+ ## Goal
99
+
100
+ <!-- What is the primary objective? -->
101
+
102
+ ## Scope
103
+
104
+ <!-- What is in scope and out of scope? -->
105
+
106
+ ## Approach
107
+
108
+ <!-- How will you achieve the goal? Key phases or milestones? -->
109
+
110
+ ## Success Criteria
111
+
112
+ <!-- How will you know when this is complete? Measurable outcomes? -->
113
+
114
+ ## Risks
115
+
116
+ <!-- What could go wrong? How will you mitigate? -->
117
+
118
+ ## Open Questions
119
+
120
+ <!-- Unresolved questions or decisions needed -->
121
+
122
+ ## References
123
+
124
+ - ID: ${id}
125
+ - Created: ${today}
126
+ `;
127
+ writeFileSync(planPath, template, { encoding: 'utf-8' });
128
+ console.log(`${LOG_PREFIX} Created plan: ${planPath}`);
129
+ return planPath;
130
+ }
131
+ /**
132
+ * Generate commit message for plan creation
133
+ *
134
+ * @param id - WU or Initiative ID
135
+ * @param title - Plan title
136
+ * @returns Commit message
137
+ */
138
+ export function getCommitMessage(id, title) {
139
+ const idLower = id.toLowerCase();
140
+ return `docs: create plan for ${idLower} - ${title}`;
141
+ }
142
+ async function main() {
143
+ const args = createWUParser({
144
+ name: 'plan-create',
145
+ description: 'Create a new plan file in repo plansDir',
146
+ options: [WU_OPTIONS.id, WU_OPTIONS.title],
147
+ required: ['id', 'title'],
148
+ allowPositionalId: true,
149
+ });
150
+ const id = args.id;
151
+ const title = args.title;
152
+ // Validate inputs
153
+ validatePlanId(id);
154
+ console.log(`${LOG_PREFIX} Creating plan for ${id}...`);
155
+ // Ensure on main for micro-worktree operations
156
+ await ensureOnMain(getGitForCwd());
157
+ try {
158
+ let createdPlanPath = '';
159
+ await withMicroWorktree({
160
+ operation: OPERATION_NAME,
161
+ id,
162
+ logPrefix: LOG_PREFIX,
163
+ pushOnly: true,
164
+ execute: async ({ worktreePath }) => {
165
+ // Create plan file
166
+ createdPlanPath = createPlan(worktreePath, id, title);
167
+ // Get relative path for commit
168
+ const planRelPath = createdPlanPath.replace(worktreePath + '/', '');
169
+ return {
170
+ commitMessage: getCommitMessage(id, title),
171
+ files: [planRelPath],
172
+ };
173
+ },
174
+ });
175
+ const planUri = getPlanUri(id);
176
+ console.log(`\n${LOG_PREFIX} Plan created successfully!`);
177
+ console.log(`\nPlan Details:`);
178
+ console.log(` ID: ${id}`);
179
+ console.log(` Title: ${title}`);
180
+ console.log(` URI: ${planUri}`);
181
+ console.log(` File: ${createdPlanPath}`);
182
+ console.log(`\nNext steps:`);
183
+ console.log(` 1. Edit the plan file with your goals and approach`);
184
+ console.log(` 2. Link to WU/initiative: pnpm plan:link --id ${id} --plan ${planUri}`);
185
+ console.log(` 3. When ready, promote: pnpm plan:promote --id ${id}`);
186
+ }
187
+ catch (error) {
188
+ die(`Plan creation failed: ${error.message}\n\n` +
189
+ `Micro-worktree cleanup was attempted automatically.\n` +
190
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
191
+ }
192
+ }
193
+ // Guard main() for testability
194
+ import { runCLI } from './cli-entry-point.js';
195
+ if (import.meta.main) {
196
+ void runCLI(main);
197
+ }
198
+ // Export for testing
199
+ export { main };
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /* eslint-disable security/detect-non-literal-fs-filename */
4
+ /**
5
+ * Plan Edit Command (WU-1313)
6
+ *
7
+ * Edits existing plan files in the repo-native plansDir.
8
+ * Supports updating or appending to specific sections.
9
+ *
10
+ * Usage:
11
+ * pnpm plan:edit --id WU-1313 --section Goal --content "New goal content"
12
+ * pnpm plan:edit --id WU-1313 --section Risks --append "- New risk"
13
+ *
14
+ * Features:
15
+ * - Updates specific sections by name
16
+ * - Supports append mode for list sections (Risks, etc.)
17
+ * - Uses micro-worktree isolation for atomic commits
18
+ * - Validates section exists before editing
19
+ *
20
+ * Context: WU-1313 (INIT-013 Plan Tooling)
21
+ */
22
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
23
+ import { die } from '@lumenflow/core/dist/error-handler.js';
24
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
27
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
28
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
29
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
30
+ import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
31
+ /** Log prefix for console output */
32
+ export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_EDIT ?? '[plan:edit]';
33
+ /** Micro-worktree operation name */
34
+ const OPERATION_NAME = 'plan-edit';
35
+ /** WU ID pattern */
36
+ const WU_ID_PATTERN = /^WU-\d+$/;
37
+ /** Initiative ID pattern */
38
+ const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
39
+ /**
40
+ * Get the path to a plan file from its ID
41
+ *
42
+ * @param id - WU or Initiative ID
43
+ * @returns Path to plan file
44
+ * @throws Error if plan not found
45
+ */
46
+ export function getPlanPath(id) {
47
+ const plansDir = WU_PATHS.PLANS_DIR();
48
+ const planPath = join(plansDir, `${id}-plan.md`);
49
+ if (!existsSync(planPath)) {
50
+ die(`Plan not found for ${id}\n\n` +
51
+ `Expected path: ${planPath}\n\n` +
52
+ `Create it first with: pnpm plan:create --id ${id} --title "Title"`);
53
+ }
54
+ return planPath;
55
+ }
56
+ /**
57
+ * Update a section in a plan file
58
+ *
59
+ * Replaces all content between the section heading and the next heading.
60
+ *
61
+ * @param planPath - Path to plan file
62
+ * @param section - Section name (without ##)
63
+ * @param content - New content for the section
64
+ * @returns True if changes were made, false if section not found
65
+ */
66
+ export function updatePlanSection(planPath, section, content) {
67
+ if (!existsSync(planPath)) {
68
+ die(`Plan file not found: ${planPath}`);
69
+ }
70
+ const text = readFileSync(planPath, { encoding: 'utf-8' });
71
+ const lines = text.split('\n');
72
+ // Find the section heading
73
+ const sectionHeading = `## ${section}`;
74
+ const headingIndex = lines.findIndex((line) => line.trim() === sectionHeading);
75
+ if (headingIndex === -1) {
76
+ console.log(`${LOG_PREFIX} Section "${section}" not found in plan`);
77
+ return false;
78
+ }
79
+ // Find the next heading (end of this section)
80
+ let nextHeadingIndex = lines.findIndex((line, i) => i > headingIndex && line.trim().startsWith('## '));
81
+ if (nextHeadingIndex === -1) {
82
+ nextHeadingIndex = lines.length;
83
+ }
84
+ // Replace the section content (keep the heading, replace content until next heading)
85
+ const beforeSection = lines.slice(0, headingIndex + 1);
86
+ const afterSection = lines.slice(nextHeadingIndex);
87
+ const newLines = [...beforeSection, '', content, '', ...afterSection];
88
+ writeFileSync(planPath, newLines.join('\n'), { encoding: 'utf-8' });
89
+ console.log(`${LOG_PREFIX} Updated "${section}" section in plan`);
90
+ return true;
91
+ }
92
+ /**
93
+ * Append content to a section in a plan file
94
+ *
95
+ * Adds content at the end of the section, before the next heading.
96
+ *
97
+ * @param planPath - Path to plan file
98
+ * @param section - Section name (without ##)
99
+ * @param content - Content to append
100
+ * @returns True if changes were made, false if section not found
101
+ */
102
+ export function appendToSection(planPath, section, content) {
103
+ if (!existsSync(planPath)) {
104
+ die(`Plan file not found: ${planPath}`);
105
+ }
106
+ const text = readFileSync(planPath, { encoding: 'utf-8' });
107
+ const lines = text.split('\n');
108
+ // Find the section heading
109
+ const sectionHeading = `## ${section}`;
110
+ const headingIndex = lines.findIndex((line) => line.trim() === sectionHeading);
111
+ if (headingIndex === -1) {
112
+ console.log(`${LOG_PREFIX} Section "${section}" not found in plan`);
113
+ return false;
114
+ }
115
+ // Find the next heading (end of this section)
116
+ let nextHeadingIndex = lines.findIndex((line, i) => i > headingIndex && line.trim().startsWith('## '));
117
+ if (nextHeadingIndex === -1) {
118
+ nextHeadingIndex = lines.length;
119
+ }
120
+ // Find the last non-empty line before the next heading
121
+ let insertIndex = nextHeadingIndex;
122
+ for (let i = nextHeadingIndex - 1; i > headingIndex; i--) {
123
+ if (lines[i].trim() !== '') {
124
+ insertIndex = i + 1;
125
+ break;
126
+ }
127
+ }
128
+ // Insert the new content
129
+ lines.splice(insertIndex, 0, content);
130
+ writeFileSync(planPath, lines.join('\n'), { encoding: 'utf-8' });
131
+ console.log(`${LOG_PREFIX} Appended to "${section}" section in plan`);
132
+ return true;
133
+ }
134
+ /**
135
+ * Generate commit message for plan edit operation
136
+ *
137
+ * @param id - WU or Initiative ID
138
+ * @param section - Section that was edited
139
+ * @returns Commit message
140
+ */
141
+ export function getCommitMessage(id, section) {
142
+ const idLower = id.toLowerCase();
143
+ return `docs: update ${section} section in ${idLower} plan`;
144
+ }
145
+ async function main() {
146
+ const SECTION_OPTION = {
147
+ name: 'section',
148
+ flags: '--section <name>',
149
+ description: 'Section name to edit (Goal, Scope, Approach, etc.)',
150
+ };
151
+ const CONTENT_OPTION = {
152
+ name: 'content',
153
+ flags: '--content <text>',
154
+ description: 'New content for the section',
155
+ };
156
+ const APPEND_OPTION = {
157
+ name: 'append',
158
+ flags: '--append <text>',
159
+ description: 'Content to append to the section (instead of replace)',
160
+ };
161
+ const args = createWUParser({
162
+ name: 'plan-edit',
163
+ description: 'Edit a section in a plan file',
164
+ options: [WU_OPTIONS.id, SECTION_OPTION, CONTENT_OPTION, APPEND_OPTION],
165
+ required: ['id', 'section'],
166
+ allowPositionalId: true,
167
+ });
168
+ const id = args.id;
169
+ const section = args.section;
170
+ const content = args.content;
171
+ const appendContent = args.append;
172
+ // Validate we have either content or append
173
+ if (!content && !appendContent) {
174
+ die('Either --content or --append is required');
175
+ }
176
+ // Validate ID format
177
+ if (!WU_ID_PATTERN.test(id) && !INIT_ID_PATTERN.test(id)) {
178
+ die(`Invalid ID format: "${id}"\n\n` + `Expected format: WU-XXX or INIT-XXX`);
179
+ }
180
+ console.log(`${LOG_PREFIX} Editing plan for ${id}...`);
181
+ // Ensure on main for micro-worktree operations
182
+ await ensureOnMain(getGitForCwd());
183
+ try {
184
+ await withMicroWorktree({
185
+ operation: OPERATION_NAME,
186
+ id,
187
+ logPrefix: LOG_PREFIX,
188
+ pushOnly: true,
189
+ execute: async ({ worktreePath }) => {
190
+ const planRelPath = join(WU_PATHS.PLANS_DIR(), `${id}-plan.md`);
191
+ const planAbsPath = join(worktreePath, planRelPath);
192
+ if (!existsSync(planAbsPath)) {
193
+ die(`Plan not found for ${id}\n\n` +
194
+ `Expected path: ${planRelPath}\n\n` +
195
+ `Create it first with: pnpm plan:create --id ${id} --title "Title"`);
196
+ }
197
+ let changed;
198
+ if (appendContent) {
199
+ changed = appendToSection(planAbsPath, section, appendContent);
200
+ }
201
+ else if (content) {
202
+ changed = updatePlanSection(planAbsPath, section, content);
203
+ }
204
+ else {
205
+ // This shouldn't happen due to earlier validation, but satisfy TS
206
+ changed = false;
207
+ }
208
+ if (!changed) {
209
+ console.warn(`${LOG_PREFIX} Section "${section}" not found - no changes made`);
210
+ }
211
+ return {
212
+ commitMessage: getCommitMessage(id, section),
213
+ files: [planRelPath],
214
+ };
215
+ },
216
+ });
217
+ console.log(`\n${LOG_PREFIX} Plan edited successfully!`);
218
+ console.log(`\nEdit Details:`);
219
+ console.log(` ID: ${id}`);
220
+ console.log(` Section: ${section}`);
221
+ console.log(` Mode: ${appendContent ? 'append' : 'replace'}`);
222
+ }
223
+ catch (error) {
224
+ die(`Plan edit failed: ${error.message}\n\n` +
225
+ `Micro-worktree cleanup was attempted automatically.\n` +
226
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
227
+ }
228
+ }
229
+ // Guard main() for testability
230
+ import { runCLI } from './cli-entry-point.js';
231
+ if (import.meta.main) {
232
+ void runCLI(main);
233
+ }
234
+ // Export for testing
235
+ export { main };
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /* eslint-disable security/detect-non-literal-fs-filename */
4
+ /**
5
+ * Plan Link Command (WU-1313)
6
+ *
7
+ * Links plan files to WUs (via spec_refs) or initiatives (via related_plan).
8
+ * This replaces the initiative:plan command for linking to initiatives.
9
+ *
10
+ * Usage:
11
+ * pnpm plan:link --id WU-1313 --plan lumenflow://plans/WU-1313-plan.md
12
+ * pnpm plan:link --id INIT-001 --plan lumenflow://plans/INIT-001-plan.md
13
+ *
14
+ * Features:
15
+ * - Auto-detects target type (WU or Initiative) from ID format
16
+ * - Updates spec_refs for WUs, related_plan for initiatives
17
+ * - Validates plan file exists before linking
18
+ * - Uses micro-worktree isolation for atomic commits
19
+ * - Idempotent: no error if already linked
20
+ *
21
+ * Context: WU-1313 (INIT-013 Plan Tooling)
22
+ */
23
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
24
+ import { die } from '@lumenflow/core/dist/error-handler.js';
25
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
28
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
29
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
30
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
31
+ import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.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.PLAN_LINK ?? '[plan:link]';
36
+ /** Micro-worktree operation name */
37
+ const OPERATION_NAME = 'plan-link';
38
+ /** LumenFlow URI scheme for plan references */
39
+ const PLAN_URI_SCHEME = 'lumenflow://plans/';
40
+ /** WU ID pattern */
41
+ const WU_ID_PATTERN = /^WU-\d+$/;
42
+ /** Initiative ID pattern */
43
+ const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
44
+ /**
45
+ * Resolve the target type from an ID
46
+ *
47
+ * @param id - ID to check (WU-XXX or INIT-XXX)
48
+ * @returns 'wu' or 'initiative'
49
+ * @throws Error if ID format is invalid
50
+ */
51
+ export function resolveTargetType(id) {
52
+ if (!id) {
53
+ die(`ID is required\n\nExpected format: WU-XXX or INIT-XXX`);
54
+ }
55
+ if (WU_ID_PATTERN.test(id)) {
56
+ return 'wu';
57
+ }
58
+ if (INIT_ID_PATTERN.test(id)) {
59
+ return 'initiative';
60
+ }
61
+ die(`Invalid ID format: "${id}"\n\n` +
62
+ `Expected format:\n` +
63
+ ` - WU ID: WU-<number> (e.g., WU-1313)\n` +
64
+ ` - Initiative ID: INIT-<alphanumeric> (e.g., INIT-001, INIT-TOOLING)`);
65
+ // TypeScript requires a return here even though die() never returns
66
+ return 'wu';
67
+ }
68
+ /**
69
+ * Validate that the plan file exists
70
+ *
71
+ * @param worktreePath - Path to repo root or worktree
72
+ * @param planUri - Plan URI (lumenflow://plans/...)
73
+ * @throws Error if plan file doesn't exist
74
+ */
75
+ export function validatePlanExists(worktreePath, planUri) {
76
+ // Extract filename from URI
77
+ const filename = planUri.replace(PLAN_URI_SCHEME, '');
78
+ const plansDir = join(worktreePath, WU_PATHS.PLANS_DIR());
79
+ const planPath = join(plansDir, filename);
80
+ if (!existsSync(planPath)) {
81
+ die(`Plan file not found: ${planPath}\n\n` +
82
+ `Create it first with: pnpm plan:create --id <ID> --title "Title"`);
83
+ }
84
+ }
85
+ /**
86
+ * Link a plan to a WU by updating spec_refs
87
+ *
88
+ * @param worktreePath - Path to repo root or worktree
89
+ * @param wuId - WU ID
90
+ * @param planUri - Plan URI
91
+ * @returns True if changes were made, false if already linked
92
+ */
93
+ export function linkPlanToWU(worktreePath, wuId, planUri) {
94
+ const wuRelPath = WU_PATHS.WU(wuId);
95
+ const wuAbsPath = join(worktreePath, wuRelPath);
96
+ if (!existsSync(wuAbsPath)) {
97
+ die(`WU not found: ${wuId}\n\nFile does not exist: ${wuAbsPath}`);
98
+ }
99
+ // Read raw YAML to preserve all fields
100
+ const rawText = readFileSync(wuAbsPath, { encoding: 'utf-8' });
101
+ const doc = parseYAML(rawText);
102
+ // Check for existing spec_refs
103
+ let specRefs = doc.spec_refs;
104
+ if (!specRefs) {
105
+ specRefs = [];
106
+ }
107
+ // Check if already linked
108
+ if (specRefs.includes(planUri)) {
109
+ console.log(`${LOG_PREFIX} Plan already linked to ${wuId} (idempotent)`);
110
+ return false;
111
+ }
112
+ // Add new plan URI
113
+ specRefs.push(planUri);
114
+ doc.spec_refs = specRefs;
115
+ // Write back
116
+ const out = stringifyYAML(doc, { lineWidth: -1 });
117
+ writeFileSync(wuAbsPath, out, { encoding: 'utf-8' });
118
+ console.log(`${LOG_PREFIX} Linked plan to ${wuId}: ${planUri}`);
119
+ return true;
120
+ }
121
+ /**
122
+ * Link a plan to an initiative by updating related_plan
123
+ *
124
+ * @param worktreePath - Path to repo root or worktree
125
+ * @param initId - Initiative ID
126
+ * @param planUri - Plan URI
127
+ * @returns True if changes were made, false if already linked
128
+ */
129
+ export function linkPlanToInitiative(worktreePath, initId, planUri) {
130
+ const initRelPath = INIT_PATHS.INITIATIVE(initId);
131
+ const initAbsPath = join(worktreePath, initRelPath);
132
+ if (!existsSync(initAbsPath)) {
133
+ die(`Initiative not found: ${initId}\n\nFile does not exist: ${initAbsPath}`);
134
+ }
135
+ // Read raw YAML to preserve all fields
136
+ const rawText = readFileSync(initAbsPath, { encoding: 'utf-8' });
137
+ const doc = parseYAML(rawText);
138
+ // Check for existing related_plan
139
+ const existingPlan = doc.related_plan;
140
+ if (existingPlan === planUri) {
141
+ console.log(`${LOG_PREFIX} Plan already linked to ${initId} (idempotent)`);
142
+ return false;
143
+ }
144
+ if (existingPlan && existingPlan !== planUri) {
145
+ console.warn(`${LOG_PREFIX} Replacing existing related_plan: ${existingPlan} -> ${planUri}`);
146
+ }
147
+ // Update related_plan
148
+ doc.related_plan = planUri;
149
+ // Write back
150
+ const out = stringifyYAML(doc, { lineWidth: -1 });
151
+ writeFileSync(initAbsPath, out, { encoding: 'utf-8' });
152
+ console.log(`${LOG_PREFIX} Linked plan to ${initId}: ${planUri}`);
153
+ return true;
154
+ }
155
+ /**
156
+ * Generate commit message for plan link operation
157
+ *
158
+ * @param id - WU or Initiative ID
159
+ * @param planUri - Plan URI
160
+ * @returns Commit message
161
+ */
162
+ export function getCommitMessage(id, planUri) {
163
+ const idLower = id.toLowerCase();
164
+ const filename = planUri.replace(PLAN_URI_SCHEME, '');
165
+ return `docs: link plan ${filename} to ${idLower}`;
166
+ }
167
+ async function main() {
168
+ const PLAN_OPTION = {
169
+ name: 'plan',
170
+ flags: '--plan <uri>',
171
+ description: 'Plan URI (lumenflow://plans/...)',
172
+ };
173
+ const args = createWUParser({
174
+ name: 'plan-link',
175
+ description: 'Link a plan file to a WU or initiative',
176
+ options: [WU_OPTIONS.id, PLAN_OPTION],
177
+ required: ['id', 'plan'],
178
+ allowPositionalId: true,
179
+ });
180
+ const id = args.id;
181
+ const planUri = args.plan;
182
+ // Resolve target type
183
+ const targetType = resolveTargetType(id);
184
+ console.log(`${LOG_PREFIX} Linking plan to ${targetType === 'wu' ? 'WU' : 'initiative'} ${id}...`);
185
+ // Ensure on main for micro-worktree operations
186
+ await ensureOnMain(getGitForCwd());
187
+ try {
188
+ await withMicroWorktree({
189
+ operation: OPERATION_NAME,
190
+ id,
191
+ logPrefix: LOG_PREFIX,
192
+ pushOnly: true,
193
+ execute: async ({ worktreePath }) => {
194
+ // Validate plan exists
195
+ validatePlanExists(worktreePath, planUri);
196
+ // Link plan based on target type
197
+ let changed;
198
+ let filePath;
199
+ if (targetType === 'wu') {
200
+ changed = linkPlanToWU(worktreePath, id, planUri);
201
+ filePath = WU_PATHS.WU(id);
202
+ }
203
+ else {
204
+ changed = linkPlanToInitiative(worktreePath, id, planUri);
205
+ filePath = INIT_PATHS.INITIATIVE(id);
206
+ }
207
+ if (!changed) {
208
+ console.log(`${LOG_PREFIX} No changes needed (already linked)`);
209
+ }
210
+ return {
211
+ commitMessage: getCommitMessage(id, planUri),
212
+ files: [filePath],
213
+ };
214
+ },
215
+ });
216
+ console.log(`\n${LOG_PREFIX} Plan linked successfully!`);
217
+ console.log(`\nLink Details:`);
218
+ console.log(` Target: ${id} (${targetType})`);
219
+ console.log(` Plan: ${planUri}`);
220
+ }
221
+ catch (error) {
222
+ die(`Plan linking failed: ${error.message}\n\n` +
223
+ `Micro-worktree cleanup was attempted automatically.\n` +
224
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
225
+ }
226
+ }
227
+ // Guard main() for testability
228
+ import { runCLI } from './cli-entry-point.js';
229
+ if (import.meta.main) {
230
+ void runCLI(main);
231
+ }
232
+ // Export for testing
233
+ export { main };