@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.
- 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 +96 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +199 -3
- 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__/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/flow-report.js +3 -2
- package/dist/gates.js +202 -2
- package/dist/init.js +720 -40
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-plan.js +3 -2
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/mem-context.js +0 -0
- package/dist/metrics-snapshot.js +3 -2
- package/dist/onboarding-smoke-test.js +400 -0
- 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/rotate-progress.js +8 -5
- package/dist/spawn-list.js +4 -3
- package/dist/state-bootstrap.js +6 -4
- package/dist/state-doctor-fix.js +5 -4
- package/dist/state-doctor.js +32 -2
- package/dist/trace-gen.js +6 -3
- 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-infer-lane.js +3 -1
- package/dist/wu-spawn.js +8 -0
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +12 -6
- package/templates/core/AGENTS.md.template +2 -2
- 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
package/dist/wu-unblock.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/dist/wu-validate.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
11
|
-
* pnpm wu:validate --all # Validate all WUs
|
|
12
|
-
* pnpm wu:validate --all --strict
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
+
"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.
|
|
146
|
-
"@lumenflow/
|
|
147
|
-
"@lumenflow/
|
|
148
|
-
"@lumenflow/agent": "2.
|
|
149
|
-
"@lumenflow/metrics": "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
|
|
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](
|
|
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 {};
|