@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.
- package/README.md +11 -8
- package/dist/__tests__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +207 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +670 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/sync-templates.js +137 -5
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +14 -1
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +11 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/AGENTS.md.template +2 -2
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- 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 };
|