@lumenflow/cli 2.3.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/__tests__/init-config-lanes.test.js +131 -0
  2. package/dist/__tests__/init-docs-structure.test.js +119 -0
  3. package/dist/__tests__/init-lane-inference.test.js +125 -0
  4. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  5. package/dist/__tests__/init-quick-ref.test.js +145 -0
  6. package/dist/__tests__/init-scripts.test.js +96 -0
  7. package/dist/__tests__/init-template-portability.test.js +97 -0
  8. package/dist/__tests__/init.test.js +199 -3
  9. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  10. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  11. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  12. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  13. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  14. package/dist/__tests__/plan-create.test.js +126 -0
  15. package/dist/__tests__/plan-edit.test.js +157 -0
  16. package/dist/__tests__/plan-link.test.js +239 -0
  17. package/dist/__tests__/plan-promote.test.js +181 -0
  18. package/dist/__tests__/wu-create-strict.test.js +118 -0
  19. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  20. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  21. package/dist/flow-bottlenecks.js +4 -2
  22. package/dist/flow-report.js +3 -2
  23. package/dist/gates.js +202 -2
  24. package/dist/init.js +720 -40
  25. package/dist/initiative-add-wu.js +112 -16
  26. package/dist/initiative-plan.js +3 -2
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/mem-context.js +0 -0
  29. package/dist/metrics-snapshot.js +3 -2
  30. package/dist/onboarding-smoke-test.js +400 -0
  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/rotate-progress.js +8 -5
  36. package/dist/spawn-list.js +4 -3
  37. package/dist/state-bootstrap.js +6 -4
  38. package/dist/state-doctor-fix.js +5 -4
  39. package/dist/state-doctor.js +32 -2
  40. package/dist/trace-gen.js +6 -3
  41. package/dist/wu-block.js +16 -5
  42. package/dist/wu-claim.js +15 -9
  43. package/dist/wu-create.js +50 -2
  44. package/dist/wu-deps.js +3 -1
  45. package/dist/wu-done.js +14 -5
  46. package/dist/wu-edit.js +35 -0
  47. package/dist/wu-infer-lane.js +3 -1
  48. package/dist/wu-spawn.js +8 -0
  49. package/dist/wu-unblock.js +34 -2
  50. package/dist/wu-validate.js +25 -17
  51. package/package.json +12 -6
  52. package/templates/core/AGENTS.md.template +2 -2
  53. package/dist/__tests__/init-plan.test.js +0 -340
  54. package/dist/agent-issues-query.d.ts +0 -16
  55. package/dist/agent-log-issue.d.ts +0 -10
  56. package/dist/agent-session-end.d.ts +0 -10
  57. package/dist/agent-session.d.ts +0 -10
  58. package/dist/backlog-prune.d.ts +0 -84
  59. package/dist/cli-entry-point.d.ts +0 -8
  60. package/dist/deps-add.d.ts +0 -91
  61. package/dist/deps-remove.d.ts +0 -17
  62. package/dist/docs-sync.d.ts +0 -50
  63. package/dist/file-delete.d.ts +0 -84
  64. package/dist/file-edit.d.ts +0 -82
  65. package/dist/file-read.d.ts +0 -92
  66. package/dist/file-write.d.ts +0 -90
  67. package/dist/flow-bottlenecks.d.ts +0 -16
  68. package/dist/flow-report.d.ts +0 -16
  69. package/dist/gates.d.ts +0 -94
  70. package/dist/git-branch.d.ts +0 -65
  71. package/dist/git-diff.d.ts +0 -58
  72. package/dist/git-log.d.ts +0 -69
  73. package/dist/git-status.d.ts +0 -58
  74. package/dist/guard-locked.d.ts +0 -62
  75. package/dist/guard-main-branch.d.ts +0 -50
  76. package/dist/guard-worktree-commit.d.ts +0 -59
  77. package/dist/index.d.ts +0 -10
  78. package/dist/init-plan.d.ts +0 -80
  79. package/dist/init-plan.js +0 -337
  80. package/dist/init.d.ts +0 -46
  81. package/dist/initiative-add-wu.d.ts +0 -22
  82. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  83. package/dist/initiative-create.d.ts +0 -28
  84. package/dist/initiative-edit.d.ts +0 -34
  85. package/dist/initiative-list.d.ts +0 -12
  86. package/dist/initiative-status.d.ts +0 -11
  87. package/dist/lumenflow-upgrade.d.ts +0 -103
  88. package/dist/mem-checkpoint.d.ts +0 -16
  89. package/dist/mem-cleanup.d.ts +0 -29
  90. package/dist/mem-create.d.ts +0 -17
  91. package/dist/mem-export.d.ts +0 -10
  92. package/dist/mem-inbox.d.ts +0 -35
  93. package/dist/mem-init.d.ts +0 -15
  94. package/dist/mem-ready.d.ts +0 -16
  95. package/dist/mem-signal.d.ts +0 -16
  96. package/dist/mem-start.d.ts +0 -16
  97. package/dist/mem-summarize.d.ts +0 -22
  98. package/dist/mem-triage.d.ts +0 -22
  99. package/dist/metrics-cli.d.ts +0 -90
  100. package/dist/metrics-snapshot.d.ts +0 -18
  101. package/dist/orchestrate-init-status.d.ts +0 -11
  102. package/dist/orchestrate-initiative.d.ts +0 -12
  103. package/dist/orchestrate-monitor.d.ts +0 -11
  104. package/dist/release.d.ts +0 -117
  105. package/dist/rotate-progress.d.ts +0 -48
  106. package/dist/session-coordinator.d.ts +0 -74
  107. package/dist/spawn-list.d.ts +0 -16
  108. package/dist/state-bootstrap.d.ts +0 -92
  109. package/dist/sync-templates.d.ts +0 -52
  110. package/dist/trace-gen.d.ts +0 -84
  111. package/dist/validate-agent-skills.d.ts +0 -50
  112. package/dist/validate-agent-sync.d.ts +0 -36
  113. package/dist/validate-backlog-sync.d.ts +0 -37
  114. package/dist/validate-skills-spec.d.ts +0 -40
  115. package/dist/validate.d.ts +0 -60
  116. package/dist/wu-block.d.ts +0 -16
  117. package/dist/wu-claim.d.ts +0 -74
  118. package/dist/wu-cleanup.d.ts +0 -35
  119. package/dist/wu-create.d.ts +0 -69
  120. package/dist/wu-delete.d.ts +0 -21
  121. package/dist/wu-deps.d.ts +0 -13
  122. package/dist/wu-done.d.ts +0 -225
  123. package/dist/wu-edit.d.ts +0 -63
  124. package/dist/wu-infer-lane.d.ts +0 -17
  125. package/dist/wu-preflight.d.ts +0 -47
  126. package/dist/wu-prune.d.ts +0 -16
  127. package/dist/wu-recover.d.ts +0 -37
  128. package/dist/wu-release.d.ts +0 -19
  129. package/dist/wu-repair.d.ts +0 -60
  130. package/dist/wu-spawn-completion.d.ts +0 -10
  131. package/dist/wu-spawn.d.ts +0 -192
  132. package/dist/wu-status.d.ts +0 -25
  133. package/dist/wu-unblock.d.ts +0 -16
  134. package/dist/wu-unlock-lane.d.ts +0 -19
  135. package/dist/wu-validate.d.ts +0 -16
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
2
3
  /**
3
4
  * WU Unblock Helper
4
5
  *
@@ -16,12 +17,15 @@
16
17
  import { existsSync, writeFileSync } from 'node:fs';
17
18
  import path from 'node:path';
18
19
  import { assertTransition } from '@lumenflow/core/dist/state-machine.js';
19
- import { checkLaneFree } from '@lumenflow/core/dist/lane-checker.js';
20
+ import { checkLaneFree, getLockPolicyForLane } from '@lumenflow/core/dist/lane-checker.js';
21
+ // WU-1325: Import lane lock functions for policy-based lock acquisition on unblock
22
+ import { acquireLaneLock } from '@lumenflow/core/dist/lane-lock.js';
20
23
  import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
21
24
  import { die } from '@lumenflow/core/dist/error-handler.js';
22
25
  import { todayISO } from '@lumenflow/core/dist/date-utils.js';
23
26
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
24
27
  import { WU_PATHS, defaultWorktreeFrom } from '@lumenflow/core/dist/wu-paths.js';
28
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
25
29
  import { readWU, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
26
30
  import { STATUS_SECTIONS, PATTERNS, LOG_PREFIX, WU_STATUS, REMOTES, BRANCHES, GIT_REFS, FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, } from '@lumenflow/core/dist/wu-constants.js';
27
31
  import { defaultBranchFrom } from '@lumenflow/core/dist/wu-done-validators.js';
@@ -88,7 +92,8 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
88
92
  ` 1. Wait for ${laneCheck.occupiedBy} to complete or block\n` +
89
93
  ` 2. Move ${id} to a different lane\n` +
90
94
  ` 3. Use --force to override (P0 emergencies only)\n\n` +
91
- `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
95
+ // WU-1311: Use config-based status path
96
+ `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" ${getConfig().directories.statusPath}`);
92
97
  }
93
98
  /**
94
99
  * Handle optional worktree creation after unblock
@@ -221,6 +226,33 @@ async function main() {
221
226
  getGitForCwd().run(`git push ${REMOTES.ORIGIN} ${BRANCHES.MAIN}`);
222
227
  }
223
228
  handleWorktreeCreation(args, doc);
229
+ // WU-1325: Re-acquire lane lock when WU is unblocked (only for lock_policy=active)
230
+ // For policy=all, lock was retained through the block cycle
231
+ // For policy=none, no lock exists to acquire
232
+ try {
233
+ if (lane && lane !== 'Unknown') {
234
+ const lockPolicy = getLockPolicyForLane(lane);
235
+ if (lockPolicy === 'active') {
236
+ const lockResult = acquireLaneLock(lane, id);
237
+ if (lockResult.acquired && !lockResult.skipped) {
238
+ console.log(`${PREFIX} Lane lock re-acquired for "${lane}" (lock_policy=active)`);
239
+ }
240
+ else if (!lockResult.acquired) {
241
+ // Lock acquisition failed - another WU claimed the lane while we were blocked
242
+ console.warn(`${PREFIX} Warning: Could not re-acquire lane lock: ${lockResult.error}`);
243
+ console.warn(`${PREFIX} Another WU may have claimed lane "${lane}" while this WU was blocked.`);
244
+ }
245
+ }
246
+ else if (lockPolicy === 'all') {
247
+ console.log(`${PREFIX} Lane lock retained for "${lane}" (lock_policy=all)`);
248
+ }
249
+ // For policy=none, no lock exists - nothing to do
250
+ }
251
+ }
252
+ catch (err) {
253
+ // Non-blocking: lock acquisition failure should not block the unblocking operation
254
+ console.warn(`${PREFIX} Warning: Could not acquire lane lock: ${err.message}`);
255
+ }
224
256
  console.log(`\n${PREFIX} Marked in progress and pushed.`);
225
257
  console.log(`- WU: ${id} — ${title}`);
226
258
  if (args.reason)
@@ -3,13 +3,15 @@
3
3
  * WU Validation Tool
4
4
  *
5
5
  * Validates WU YAML files against schema and checks for quality warnings.
6
- * Returns exit code 0 if valid (warnings are advisory, not blocking).
7
- * Returns exit code 1 only for schema errors.
6
+ *
7
+ * WU-1329: Strict mode is now the DEFAULT behavior.
8
+ * - Warnings are treated as errors by default
9
+ * - Use --no-strict to restore legacy advisory-only warnings behavior
8
10
  *
9
11
  * Usage:
10
- * pnpm wu:validate --id WU-123 # Validate specific WU
11
- * pnpm wu:validate --all # Validate all WUs
12
- * pnpm wu:validate --all --strict # Fail on warnings too
12
+ * pnpm wu:validate --id WU-123 # Validate with strict mode (default)
13
+ * pnpm wu:validate --all # Validate all WUs with strict mode
14
+ * pnpm wu:validate --all --no-strict # Warnings are advisory (legacy behavior)
13
15
  *
14
16
  * @see {@link packages/@lumenflow/cli/src/lib/wu-schema.ts} - Schema definitions
15
17
  */
@@ -27,12 +29,14 @@ const LOG_PREFIX = '[wu:validate]';
27
29
  /**
28
30
  * Validate a single WU file
29
31
  *
32
+ * WU-1329: strict defaults to true (warnings treated as errors)
33
+ *
30
34
  * @param {string} wuPath - Path to WU YAML file
31
35
  * @param {object} options - Validation options
32
- * @param {boolean} options.strict - Treat warnings as errors
36
+ * @param {boolean} options.strict - Treat warnings as errors (default: true)
33
37
  * @returns {{valid: boolean, warnings: string[], errors: string[]}}
34
38
  */
35
- function validateSingleWU(wuPath, { strict = false } = {}) {
39
+ function validateSingleWU(wuPath, { strict = true } = {}) {
36
40
  const errors = [];
37
41
  const warnings = [];
38
42
  // Read and parse YAML
@@ -74,11 +78,13 @@ function validateSingleWU(wuPath, { strict = false } = {}) {
74
78
  /**
75
79
  * Validate all WU files
76
80
  *
81
+ * WU-1329: strict defaults to true (warnings treated as errors)
82
+ *
77
83
  * @param {object} options - Validation options
78
- * @param {boolean} options.strict - Treat warnings as errors
84
+ * @param {boolean} options.strict - Treat warnings as errors (default: true)
79
85
  * @returns {{totalValid: number, totalInvalid: number, totalWarnings: number, results: object[]}}
80
86
  */
81
- function validateAllWUs({ strict = false } = {}) {
87
+ function validateAllWUs({ strict = true } = {}) {
82
88
  const wuDir = WU_PATHS.WU_DIR();
83
89
  if (!existsSync(wuDir)) {
84
90
  die(`WU directory not found: ${wuDir}`);
@@ -110,7 +116,7 @@ function validateAllWUs({ strict = false } = {}) {
110
116
  async function main() {
111
117
  const args = createWUParser({
112
118
  name: 'wu-validate',
113
- description: 'Validate WU YAML files against schema',
119
+ description: 'Validate WU YAML files against schema (strict mode by default, WU-1329)',
114
120
  options: [
115
121
  WU_OPTIONS.id,
116
122
  {
@@ -119,17 +125,19 @@ async function main() {
119
125
  type: 'boolean',
120
126
  description: 'Validate all WUs',
121
127
  },
122
- {
123
- name: 'strict',
124
- flags: '-s, --strict',
125
- type: 'boolean',
126
- description: 'Treat warnings as errors',
127
- },
128
+ // WU-1329: Change from --strict to --no-strict (strict is now default)
129
+ WU_OPTIONS.noStrict,
128
130
  ],
129
131
  required: [],
130
132
  allowPositionalId: true,
131
133
  });
132
- const { id, all, strict } = args;
134
+ const { id, all, noStrict } = args;
135
+ // WU-1329: Strict mode is the default, --no-strict opts out
136
+ const strict = !noStrict;
137
+ // WU-1329: Log when strict validation is bypassed
138
+ if (noStrict) {
139
+ console.warn(`${LOG_PREFIX} WARNING: strict validation bypassed (--no-strict). Warnings will be advisory only.`);
140
+ }
133
141
  if (!id && !all) {
134
142
  die('Must specify --id WU-XXX or --all');
135
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/cli",
3
- "version": "2.3.2",
3
+ "version": "2.5.0",
4
4
  "description": "Command-line interface for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -61,6 +61,7 @@
61
61
  "mem-export": "./dist/mem-export.js",
62
62
  "mem-signal": "./dist/mem-signal.js",
63
63
  "mem-cleanup": "./dist/mem-cleanup.js",
64
+ "mem-context": "./dist/mem-context.js",
64
65
  "signal-cleanup": "./dist/signal-cleanup.js",
65
66
  "mem-create": "./dist/mem-create.js",
66
67
  "mem-inbox": "./dist/mem-inbox.js",
@@ -72,7 +73,12 @@
72
73
  "initiative-list": "./dist/initiative-list.js",
73
74
  "initiative-status": "./dist/initiative-status.js",
74
75
  "initiative-add-wu": "./dist/initiative-add-wu.js",
76
+ "initiative-plan": "./dist/initiative-plan.js",
75
77
  "init-plan": "./dist/init-plan.js",
78
+ "plan-create": "./dist/plan-create.js",
79
+ "plan-link": "./dist/plan-link.js",
80
+ "plan-edit": "./dist/plan-edit.js",
81
+ "plan-promote": "./dist/plan-promote.js",
76
82
  "agent-session": "./dist/agent-session.js",
77
83
  "agent-session-end": "./dist/agent-session-end.js",
78
84
  "agent-log-issue": "./dist/agent-log-issue.js",
@@ -142,11 +148,11 @@
142
148
  "pretty-ms": "^9.2.0",
143
149
  "simple-git": "^3.30.0",
144
150
  "yaml": "^2.8.2",
145
- "@lumenflow/core": "2.3.2",
146
- "@lumenflow/memory": "2.3.2",
147
- "@lumenflow/initiatives": "2.3.2",
148
- "@lumenflow/agent": "2.3.2",
149
- "@lumenflow/metrics": "2.3.2"
151
+ "@lumenflow/core": "2.5.0",
152
+ "@lumenflow/initiatives": "2.5.0",
153
+ "@lumenflow/memory": "2.5.0",
154
+ "@lumenflow/agent": "2.5.0",
155
+ "@lumenflow/metrics": "2.5.0"
150
156
  },
151
157
  "devDependencies": {
152
158
  "@vitest/coverage-v8": "^4.0.17",
@@ -17,11 +17,11 @@ cd worktrees/<lane>-wu-xxxx
17
17
  pnpm gates
18
18
 
19
19
  # 3. Complete (ALWAYS run this!)
20
- cd {{PROJECT_ROOT}}
20
+ cd <project-root>
21
21
  pnpm wu:done --id WU-XXXX
22
22
  ```
23
23
 
24
- > **Complete CLI reference:** See [quick-ref-commands.md](ai/onboarding/quick-ref-commands.md)
24
+ > **Complete CLI reference:** See [quick-ref-commands.md]({{QUICK_REF_LINK}})
25
25
 
26
26
  ---
27
27
 
@@ -1,340 +0,0 @@
1
- /**
2
- * Tests for init:plan command (WU-1105)
3
- *
4
- * The init:plan command links plan files to initiatives by setting
5
- * the `related_plan` field in the initiative YAML.
6
- *
7
- * TDD: These tests are written BEFORE the implementation.
8
- */
9
- import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
10
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { tmpdir } from 'node:os';
13
- import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
14
- // Pre-import the module to ensure coverage tracking includes the module itself
15
- let initPlanModule;
16
- beforeAll(async () => {
17
- initPlanModule = await import('../init-plan.js');
18
- });
19
- // Mock modules before importing the module under test
20
- const mockGit = {
21
- branch: vi.fn().mockResolvedValue({ current: 'main' }),
22
- status: vi.fn().mockResolvedValue({ isClean: () => true }),
23
- };
24
- vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
25
- getGitForCwd: vi.fn(() => mockGit),
26
- }));
27
- vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
28
- ensureOnMain: vi.fn().mockResolvedValue(undefined),
29
- }));
30
- vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
31
- withMicroWorktree: vi.fn(async ({ execute }) => {
32
- // Simulate micro-worktree by executing in temp dir
33
- const tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
34
- mkdirSync(tempDir, { recursive: true });
35
- try {
36
- await execute({ worktreePath: tempDir });
37
- }
38
- finally {
39
- // Cleanup handled by test
40
- }
41
- }),
42
- }));
43
- describe('init:plan command', () => {
44
- let tempDir;
45
- let originalCwd;
46
- beforeEach(() => {
47
- tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
48
- mkdirSync(tempDir, { recursive: true });
49
- originalCwd = process.cwd();
50
- });
51
- afterEach(() => {
52
- process.chdir(originalCwd);
53
- if (existsSync(tempDir)) {
54
- rmSync(tempDir, { recursive: true, force: true });
55
- }
56
- vi.clearAllMocks();
57
- });
58
- describe('validateInitIdFormat', () => {
59
- it('should accept valid INIT-NNN format', async () => {
60
- const { validateInitIdFormat } = await import('../init-plan.js');
61
- // Should not throw
62
- expect(() => validateInitIdFormat('INIT-001')).not.toThrow();
63
- expect(() => validateInitIdFormat('INIT-123')).not.toThrow();
64
- });
65
- it('should accept valid INIT-NAME format', async () => {
66
- const { validateInitIdFormat } = await import('../init-plan.js');
67
- expect(() => validateInitIdFormat('INIT-TOOLING')).not.toThrow();
68
- expect(() => validateInitIdFormat('INIT-A1')).not.toThrow();
69
- });
70
- it('should reject invalid formats', async () => {
71
- const { validateInitIdFormat } = await import('../init-plan.js');
72
- expect(() => validateInitIdFormat('init-001')).toThrow();
73
- expect(() => validateInitIdFormat('INIT001')).toThrow();
74
- expect(() => validateInitIdFormat('WU-001')).toThrow();
75
- expect(() => validateInitIdFormat('')).toThrow();
76
- });
77
- });
78
- describe('validatePlanPath', () => {
79
- it('should accept existing markdown files', async () => {
80
- const { validatePlanPath } = await import('../init-plan.js');
81
- const planPath = join(tempDir, 'test-plan.md');
82
- writeFileSync(planPath, '# Test Plan');
83
- // Should not throw
84
- expect(() => validatePlanPath(planPath)).not.toThrow();
85
- });
86
- it('should reject non-existent files when not creating', async () => {
87
- const { validatePlanPath } = await import('../init-plan.js');
88
- const planPath = join(tempDir, 'nonexistent.md');
89
- expect(() => validatePlanPath(planPath)).toThrow();
90
- });
91
- it('should reject non-markdown files', async () => {
92
- const { validatePlanPath } = await import('../init-plan.js');
93
- const planPath = join(tempDir, 'test-plan.txt');
94
- writeFileSync(planPath, 'Test Plan');
95
- expect(() => validatePlanPath(planPath)).toThrow();
96
- });
97
- });
98
- describe('formatPlanUri', () => {
99
- it('should format plan path as lumenflow:// URI', async () => {
100
- const { formatPlanUri } = await import('../init-plan.js');
101
- expect(formatPlanUri('docs/04-operations/plans/my-plan.md')).toBe('lumenflow://plans/my-plan.md');
102
- });
103
- it('should handle nested paths', async () => {
104
- const { formatPlanUri } = await import('../init-plan.js');
105
- expect(formatPlanUri('docs/04-operations/plans/subdir/nested-plan.md')).toBe('lumenflow://plans/subdir/nested-plan.md');
106
- });
107
- it('should handle paths not in standard location', async () => {
108
- const { formatPlanUri } = await import('../init-plan.js');
109
- // Should still create a URI even for non-standard paths
110
- expect(formatPlanUri('/absolute/path/custom-plan.md')).toBe('lumenflow://plans/custom-plan.md');
111
- });
112
- });
113
- describe('checkInitiativeExists', () => {
114
- it('should return initiative doc if found', async () => {
115
- const { checkInitiativeExists } = await import('../init-plan.js');
116
- // Create a mock initiative file
117
- const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
118
- mkdirSync(initDir, { recursive: true });
119
- const initPath = join(initDir, 'INIT-001.yaml');
120
- const initDoc = {
121
- id: 'INIT-001',
122
- slug: 'test-initiative',
123
- title: 'Test Initiative',
124
- status: 'open',
125
- created: '2026-01-25',
126
- };
127
- writeFileSync(initPath, stringifyYAML(initDoc));
128
- process.chdir(tempDir);
129
- const result = checkInitiativeExists('INIT-001');
130
- expect(result.id).toBe('INIT-001');
131
- });
132
- it('should throw if initiative not found', async () => {
133
- const { checkInitiativeExists } = await import('../init-plan.js');
134
- process.chdir(tempDir);
135
- expect(() => checkInitiativeExists('INIT-999')).toThrow();
136
- });
137
- });
138
- describe('updateInitiativeWithPlan', () => {
139
- it('should add related_plan field to initiative', async () => {
140
- const { updateInitiativeWithPlan } = await import('../init-plan.js');
141
- // Setup mock initiative
142
- const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
143
- mkdirSync(initDir, { recursive: true });
144
- const initPath = join(initDir, 'INIT-001.yaml');
145
- const initDoc = {
146
- id: 'INIT-001',
147
- slug: 'test-initiative',
148
- title: 'Test Initiative',
149
- status: 'open',
150
- created: '2026-01-25',
151
- };
152
- writeFileSync(initPath, stringifyYAML(initDoc));
153
- // Update initiative
154
- const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md');
155
- expect(changed).toBe(true);
156
- // Verify the file was updated
157
- const updated = parseYAML(readFileSync(initPath, 'utf-8'));
158
- expect(updated.related_plan).toBe('lumenflow://plans/test-plan.md');
159
- });
160
- it('should return false if plan already linked (idempotent)', async () => {
161
- const { updateInitiativeWithPlan } = await import('../init-plan.js');
162
- // Setup mock initiative with existing plan
163
- const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
164
- mkdirSync(initDir, { recursive: true });
165
- const initPath = join(initDir, 'INIT-001.yaml');
166
- const initDoc = {
167
- id: 'INIT-001',
168
- slug: 'test-initiative',
169
- title: 'Test Initiative',
170
- status: 'open',
171
- created: '2026-01-25',
172
- related_plan: 'lumenflow://plans/test-plan.md',
173
- };
174
- writeFileSync(initPath, stringifyYAML(initDoc));
175
- // Update initiative with same plan
176
- const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md');
177
- expect(changed).toBe(false);
178
- });
179
- it('should warn but proceed if different plan already linked', async () => {
180
- const { updateInitiativeWithPlan } = await import('../init-plan.js');
181
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
182
- // Setup mock initiative with different plan
183
- const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
184
- mkdirSync(initDir, { recursive: true });
185
- const initPath = join(initDir, 'INIT-001.yaml');
186
- const initDoc = {
187
- id: 'INIT-001',
188
- slug: 'test-initiative',
189
- title: 'Test Initiative',
190
- status: 'open',
191
- created: '2026-01-25',
192
- related_plan: 'lumenflow://plans/old-plan.md',
193
- };
194
- writeFileSync(initPath, stringifyYAML(initDoc));
195
- // Update initiative with new plan
196
- const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/new-plan.md');
197
- expect(changed).toBe(true);
198
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Replacing existing related_plan'));
199
- consoleSpy.mockRestore();
200
- });
201
- });
202
- describe('createPlanTemplate', () => {
203
- it('should create a plan template file', async () => {
204
- const { createPlanTemplate } = await import('../init-plan.js');
205
- const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
206
- mkdirSync(plansDir, { recursive: true });
207
- const planPath = createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative');
208
- expect(existsSync(planPath)).toBe(true);
209
- const content = readFileSync(planPath, 'utf-8');
210
- expect(content).toContain('# INIT-001');
211
- expect(content).toContain('Test Initiative');
212
- expect(content).toContain('## Goal');
213
- expect(content).toContain('## Scope');
214
- });
215
- it('should not overwrite existing plan file', async () => {
216
- const { createPlanTemplate } = await import('../init-plan.js');
217
- const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
218
- mkdirSync(plansDir, { recursive: true });
219
- // Create existing file
220
- const existingPath = join(plansDir, 'INIT-001-test-initiative.md');
221
- writeFileSync(existingPath, '# Existing Content');
222
- expect(() => createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative')).toThrow();
223
- });
224
- });
225
- describe('LOG_PREFIX', () => {
226
- it('should use correct log prefix', async () => {
227
- const { LOG_PREFIX } = await import('../init-plan.js');
228
- expect(LOG_PREFIX).toBe('[init:plan]');
229
- });
230
- });
231
- describe('getCommitMessage', () => {
232
- it('should generate correct commit message', async () => {
233
- const { getCommitMessage } = await import('../init-plan.js');
234
- expect(getCommitMessage('INIT-001', 'lumenflow://plans/my-plan.md')).toBe('docs: link plan my-plan.md to init-001');
235
- });
236
- it('should handle nested plan paths', async () => {
237
- const { getCommitMessage } = await import('../init-plan.js');
238
- expect(getCommitMessage('INIT-TOOLING', 'lumenflow://plans/subdir/nested-plan.md')).toBe('docs: link plan subdir/nested-plan.md to init-tooling');
239
- });
240
- });
241
- describe('updateInitiativeWithPlan ID mismatch', () => {
242
- it('should throw if initiative ID does not match', async () => {
243
- const { updateInitiativeWithPlan } = await import('../init-plan.js');
244
- // Setup mock initiative with different ID
245
- const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
246
- mkdirSync(initDir, { recursive: true });
247
- const initPath = join(initDir, 'INIT-001.yaml');
248
- const initDoc = {
249
- id: 'INIT-002', // Wrong ID
250
- slug: 'test-initiative',
251
- title: 'Test Initiative',
252
- status: 'open',
253
- created: '2026-01-25',
254
- };
255
- writeFileSync(initPath, stringifyYAML(initDoc));
256
- expect(() => updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md')).toThrow();
257
- });
258
- });
259
- });
260
- describe('init:plan CLI integration', () => {
261
- it('should require --initiative flag', async () => {
262
- // This test verifies that the CLI requires the initiative flag
263
- // The actual CLI integration is tested via subprocess
264
- const { WU_OPTIONS } = await import('@lumenflow/core/dist/arg-parser.js');
265
- expect(WU_OPTIONS.initiative).toBeDefined();
266
- expect(WU_OPTIONS.initiative.flags).toContain('--initiative');
267
- });
268
- it('should export main function for CLI entry', async () => {
269
- const initPlan = await import('../init-plan.js');
270
- expect(typeof initPlan.main).toBe('function');
271
- });
272
- it('should export all required functions', async () => {
273
- const initPlan = await import('../init-plan.js');
274
- expect(typeof initPlan.validateInitIdFormat).toBe('function');
275
- expect(typeof initPlan.validatePlanPath).toBe('function');
276
- expect(typeof initPlan.formatPlanUri).toBe('function');
277
- expect(typeof initPlan.checkInitiativeExists).toBe('function');
278
- expect(typeof initPlan.updateInitiativeWithPlan).toBe('function');
279
- expect(typeof initPlan.createPlanTemplate).toBe('function');
280
- expect(typeof initPlan.getCommitMessage).toBe('function');
281
- expect(typeof initPlan.LOG_PREFIX).toBe('string');
282
- });
283
- });
284
- describe('createPlanTemplate edge cases', () => {
285
- let tempDir;
286
- let originalCwd;
287
- beforeEach(() => {
288
- tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
289
- mkdirSync(tempDir, { recursive: true });
290
- originalCwd = process.cwd();
291
- });
292
- afterEach(() => {
293
- process.chdir(originalCwd);
294
- if (existsSync(tempDir)) {
295
- rmSync(tempDir, { recursive: true, force: true });
296
- }
297
- vi.clearAllMocks();
298
- });
299
- it('should create plans directory if it does not exist', async () => {
300
- const { createPlanTemplate } = await import('../init-plan.js');
301
- // Do NOT pre-create the plans directory
302
- const planPath = createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative');
303
- expect(existsSync(planPath)).toBe(true);
304
- expect(planPath).toContain('docs/04-operations/plans');
305
- });
306
- it('should truncate long titles in filename', async () => {
307
- const { createPlanTemplate } = await import('../init-plan.js');
308
- const longTitle = 'This is an extremely long initiative title that should be truncated in the filename';
309
- const planPath = createPlanTemplate(tempDir, 'INIT-001', longTitle);
310
- expect(existsSync(planPath)).toBe(true);
311
- // Filename should be truncated
312
- const filename = planPath.split('/').pop() || '';
313
- // INIT-001- is 9 chars, .md is 3 chars, slug should be max 30 chars
314
- expect(filename.length).toBeLessThanOrEqual(9 + 30 + 3);
315
- });
316
- it('should handle special characters in title', async () => {
317
- const { createPlanTemplate } = await import('../init-plan.js');
318
- const specialTitle = "Test's Initiative: (Special) Chars! @#$%";
319
- const planPath = createPlanTemplate(tempDir, 'INIT-001', specialTitle);
320
- expect(existsSync(planPath)).toBe(true);
321
- // Filename should only have kebab-case characters
322
- expect(planPath).toMatch(/INIT-001-[a-z0-9-]+\.md$/);
323
- });
324
- });
325
- /**
326
- * Note on main() function testing:
327
- *
328
- * The main() function is intentionally not unit-tested because:
329
- * 1. It calls die() which invokes process.exit() - difficult to mock without complex test infrastructure
330
- * 2. It involves micro-worktree operations with git
331
- * 3. All business logic functions it calls ARE thoroughly tested above
332
- *
333
- * The main() function is integration/orchestration code that composes the tested helper functions.
334
- * Integration testing via subprocess (pnpm init:plan) is the appropriate testing strategy for main().
335
- *
336
- * Coverage statistics:
337
- * - All exported helper functions: ~100% coverage
338
- * - main() function: Not unit tested (orchestration code)
339
- * - Overall file coverage: ~50% (acceptable for CLI commands)
340
- */
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent Issues Query CLI (WU-1018)
4
- *
5
- * Query and display logged agent incidents/issues.
6
- *
7
- * Usage:
8
- * pnpm agent:issues-query summary # Summary of last 7 days
9
- * pnpm agent:issues-query summary --since 30 # Summary of last 30 days
10
- * pnpm agent:issues-query summary --category tooling
11
- * pnpm agent:issues-query summary --severity blocker
12
- *
13
- * @module agent-issues-query
14
- * @see {@link @lumenflow/agent}
15
- */
16
- export {};
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent Log Issue CLI
4
- *
5
- * Logs a workflow issue or incident during agent execution.
6
- *
7
- * Usage:
8
- * pnpm agent:log-issue --category workflow --severity minor --title "..." --description "..."
9
- */
10
- export {};
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent Session End CLI
4
- *
5
- * Ends the current agent session and returns summary.
6
- *
7
- * Usage:
8
- * pnpm agent:session:end
9
- */
10
- export {};
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent Session Start CLI
4
- *
5
- * Starts an agent session for tracking WU execution.
6
- *
7
- * Usage:
8
- * pnpm agent:session --wu WU-1234 --tier 2
9
- */
10
- export {};