@lumenflow/cli 1.6.0 → 2.0.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__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/package.json +34 -6
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file guard-locked.ts
|
|
4
|
+
* @description Guard that prevents changes to locked WUs (WU-1111)
|
|
5
|
+
*
|
|
6
|
+
* Validates that a WU is not locked before allowing modifications.
|
|
7
|
+
* Used by git hooks and wu: commands to enforce workflow discipline.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* guard-locked WU-123 # Check if WU-123 is locked
|
|
11
|
+
* guard-locked --wu WU-123 # Same with explicit flag
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - WU is not locked (safe to proceed)
|
|
15
|
+
* 1 - WU is locked (block operation)
|
|
16
|
+
*
|
|
17
|
+
* @see {@link docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md} - WU lifecycle
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
23
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
24
|
+
import { PATTERNS, FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
|
|
25
|
+
const LOG_PREFIX = '[guard-locked]';
|
|
26
|
+
/**
|
|
27
|
+
* Check if a WU is locked
|
|
28
|
+
*
|
|
29
|
+
* @param wuPath - Path to WU YAML file
|
|
30
|
+
* @returns true if WU has locked: true, false otherwise
|
|
31
|
+
* @throws Error if WU file does not exist or cannot be parsed
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* if (isWULocked('/path/to/WU-123.yaml')) {
|
|
35
|
+
* console.log('WU is locked, cannot modify');
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export function isWULocked(wuPath) {
|
|
39
|
+
if (!existsSync(wuPath)) {
|
|
40
|
+
throw new Error(`WU file not found: ${wuPath}`);
|
|
41
|
+
}
|
|
42
|
+
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
43
|
+
const doc = parseYAML(content);
|
|
44
|
+
return doc.locked === true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Assert that a WU is not locked
|
|
48
|
+
*
|
|
49
|
+
* @param wuPath - Path to WU YAML file
|
|
50
|
+
* @throws Error if WU is locked, with actionable fix instructions
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* try {
|
|
54
|
+
* assertWUNotLocked('/path/to/WU-123.yaml');
|
|
55
|
+
* // Safe to modify
|
|
56
|
+
* } catch (error) {
|
|
57
|
+
* console.error(error.message);
|
|
58
|
+
* process.exit(1);
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
export function assertWUNotLocked(wuPath) {
|
|
62
|
+
if (!existsSync(wuPath)) {
|
|
63
|
+
throw new Error(`WU file not found: ${wuPath}`);
|
|
64
|
+
}
|
|
65
|
+
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
66
|
+
const doc = parseYAML(content);
|
|
67
|
+
if (doc.locked === true) {
|
|
68
|
+
const wuId = doc.id || path.basename(wuPath, '.yaml');
|
|
69
|
+
throw new Error(`${LOG_PREFIX} WU ${wuId} is locked.
|
|
70
|
+
|
|
71
|
+
Locked WUs cannot be modified. This prevents accidental changes to completed work.
|
|
72
|
+
|
|
73
|
+
If you need to modify this WU:
|
|
74
|
+
1. Check if modification is really necessary (locked WUs are done)
|
|
75
|
+
2. Use wu:unlock to unlock the WU first:
|
|
76
|
+
pnpm wu:unlock --id ${wuId} --reason "reason for unlocking"
|
|
77
|
+
|
|
78
|
+
For more information:
|
|
79
|
+
See docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if a WU ID is locked by looking up the YAML file
|
|
85
|
+
*
|
|
86
|
+
* @param wuId - WU ID (e.g., "WU-123")
|
|
87
|
+
* @returns true if WU has locked: true, false otherwise
|
|
88
|
+
* @throws Error if WU file does not exist
|
|
89
|
+
*/
|
|
90
|
+
export function isWUIdLocked(wuId) {
|
|
91
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
92
|
+
return isWULocked(wuPath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Assert that a WU ID is not locked
|
|
96
|
+
*
|
|
97
|
+
* @param wuId - WU ID (e.g., "WU-123")
|
|
98
|
+
* @throws Error if WU is locked
|
|
99
|
+
*/
|
|
100
|
+
export function assertWUIdNotLocked(wuId) {
|
|
101
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
102
|
+
assertWUNotLocked(wuPath);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Main CLI entry point
|
|
106
|
+
*/
|
|
107
|
+
async function main() {
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
// Parse arguments
|
|
110
|
+
let wuId;
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
if (arg === '--wu' || arg === '--id') {
|
|
114
|
+
wuId = args[++i];
|
|
115
|
+
}
|
|
116
|
+
else if (arg === '--help' || arg === '-h') {
|
|
117
|
+
console.log(`Usage: guard-locked [--wu] WU-XXX
|
|
118
|
+
|
|
119
|
+
Check if a WU is locked. Exits with code 1 if locked.
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--wu, --id WU-XXX WU ID to check
|
|
123
|
+
-h, --help Show this help message
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
guard-locked WU-123
|
|
127
|
+
guard-locked --wu WU-123
|
|
128
|
+
`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
else if (PATTERNS.WU_ID.test(arg.toUpperCase())) {
|
|
132
|
+
wuId = arg.toUpperCase();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!wuId) {
|
|
136
|
+
console.error(`${LOG_PREFIX} Error: WU ID required`);
|
|
137
|
+
console.error('Usage: guard-locked [--wu] WU-XXX');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
// Normalize WU ID
|
|
141
|
+
wuId = wuId.toUpperCase();
|
|
142
|
+
if (!PATTERNS.WU_ID.test(wuId)) {
|
|
143
|
+
console.error(`${LOG_PREFIX} Invalid WU ID: ${wuId}`);
|
|
144
|
+
console.error('Expected format: WU-123');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
if (isWUIdLocked(wuId)) {
|
|
149
|
+
console.error(`${LOG_PREFIX} ${wuId} is locked`);
|
|
150
|
+
console.error('');
|
|
151
|
+
console.error('Locked WUs cannot be modified.');
|
|
152
|
+
console.error(`To unlock: pnpm wu:unlock --id ${wuId} --reason "your reason"`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
console.log(`${LOG_PREFIX} ${wuId} is not locked (OK)`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error(`${LOG_PREFIX} Error: ${error.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Guard main() for testability
|
|
164
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
165
|
+
main().catch((error) => {
|
|
166
|
+
console.error(`${LOG_PREFIX} Unexpected error:`, error);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Guard Main Branch CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides branch protection checks for WU workflow:
|
|
6
|
+
* - Blocks operations on main/master branches
|
|
7
|
+
* - Blocks operations on lane branches (require worktree)
|
|
8
|
+
* - Optionally allows agent branches
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node guard-main-branch.js [--allow-agent-branch] [--strict]
|
|
12
|
+
*
|
|
13
|
+
* WU-1109: INIT-003 Phase 4b - Migrate git operations
|
|
14
|
+
*/
|
|
15
|
+
import { createGitForPath, getGitForCwd, isAgentBranch, getConfig } from '@lumenflow/core';
|
|
16
|
+
/**
|
|
17
|
+
* Parse command line arguments for guard-main-branch
|
|
18
|
+
*/
|
|
19
|
+
export function parseGuardMainBranchArgs(argv) {
|
|
20
|
+
const args = {};
|
|
21
|
+
// Skip node and script name
|
|
22
|
+
const cliArgs = argv.slice(2);
|
|
23
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
24
|
+
const arg = cliArgs[i];
|
|
25
|
+
if (arg === '--help' || arg === '-h') {
|
|
26
|
+
args.help = true;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === '--allow-agent-branch') {
|
|
29
|
+
args.allowAgentBranch = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '--strict') {
|
|
32
|
+
args.strict = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === '--base-dir') {
|
|
35
|
+
args.baseDir = cliArgs[++i];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get lane branch pattern from config
|
|
42
|
+
*/
|
|
43
|
+
function getLaneBranchPattern() {
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
const prefix = config?.git?.laneBranchPrefix ?? 'lane/';
|
|
46
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
return new RegExp(`^${escaped}`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get protected branches from config
|
|
51
|
+
*/
|
|
52
|
+
function getProtectedBranches() {
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
const mainBranch = config?.git?.mainBranch ?? 'main';
|
|
55
|
+
// Always include master for legacy compatibility
|
|
56
|
+
const protectedSet = new Set([mainBranch, 'master']);
|
|
57
|
+
return Array.from(protectedSet);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a branch is a lane branch (requires worktree)
|
|
61
|
+
*/
|
|
62
|
+
function isLaneBranch(branch) {
|
|
63
|
+
return getLaneBranchPattern().test(branch);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Guard against operations on protected branches
|
|
67
|
+
*/
|
|
68
|
+
export async function guardMainBranch(args) {
|
|
69
|
+
try {
|
|
70
|
+
const git = args.baseDir ? createGitForPath(args.baseDir) : getGitForCwd();
|
|
71
|
+
const currentBranch = await git.getCurrentBranch();
|
|
72
|
+
// Handle detached HEAD
|
|
73
|
+
if (currentBranch === 'HEAD' || !currentBranch) {
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
isProtected: true,
|
|
77
|
+
currentBranch: 'HEAD (detached)',
|
|
78
|
+
reason: 'Detached HEAD state is protected - checkout a branch',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const protectedBranches = getProtectedBranches();
|
|
82
|
+
// Check if on main/master
|
|
83
|
+
if (protectedBranches.includes(currentBranch)) {
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
isProtected: true,
|
|
87
|
+
currentBranch,
|
|
88
|
+
reason: `Branch '${currentBranch}' is protected - use a worktree for WU work`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Check if on a lane branch (requires worktree discipline)
|
|
92
|
+
if (isLaneBranch(currentBranch)) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
isProtected: true,
|
|
96
|
+
currentBranch,
|
|
97
|
+
reason: `Lane branch '${currentBranch}' requires worktree - use 'pnpm wu:claim' to create worktree`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Check agent branch if not explicitly allowed
|
|
101
|
+
if (!args.allowAgentBranch) {
|
|
102
|
+
const isAgent = await isAgentBranch(currentBranch);
|
|
103
|
+
if (isAgent) {
|
|
104
|
+
// Agent branches are allowed by default (unless --strict)
|
|
105
|
+
if (args.strict) {
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
isProtected: true,
|
|
109
|
+
currentBranch,
|
|
110
|
+
reason: `Agent branch '${currentBranch}' is protected in strict mode`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Allow agent branch
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
isProtected: false,
|
|
117
|
+
currentBranch,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Branch is not protected
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
isProtected: false,
|
|
125
|
+
currentBranch,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
isProtected: true, // Fail-closed
|
|
133
|
+
error: errorMessage,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Print help message
|
|
139
|
+
*/
|
|
140
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
141
|
+
function printHelp() {
|
|
142
|
+
console.log(`
|
|
143
|
+
Usage: guard-main-branch [options]
|
|
144
|
+
|
|
145
|
+
Check if current branch is protected and block operations.
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
--base-dir <dir> Base directory for git operations
|
|
149
|
+
--allow-agent-branch Allow operations on agent branches
|
|
150
|
+
--strict Block all protected branches (including agent)
|
|
151
|
+
-h, --help Show this help message
|
|
152
|
+
|
|
153
|
+
Exit codes:
|
|
154
|
+
0 - Branch is not protected (safe to proceed)
|
|
155
|
+
1 - Branch is protected (operation blocked)
|
|
156
|
+
|
|
157
|
+
Protected branches:
|
|
158
|
+
- main/master: Always protected
|
|
159
|
+
- lane/*: Requires worktree (use wu:claim)
|
|
160
|
+
- Agent branches: Allowed unless --strict
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
guard-main-branch
|
|
164
|
+
guard-main-branch --allow-agent-branch
|
|
165
|
+
guard-main-branch --strict
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Main entry point
|
|
170
|
+
*/
|
|
171
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
172
|
+
async function main() {
|
|
173
|
+
const args = parseGuardMainBranchArgs(process.argv);
|
|
174
|
+
if (args.help) {
|
|
175
|
+
printHelp();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const result = await guardMainBranch(args);
|
|
179
|
+
if (result.success) {
|
|
180
|
+
if (result.isProtected) {
|
|
181
|
+
console.error(`[guard-main-branch] BLOCKED: ${result.reason}`);
|
|
182
|
+
console.error(`Current branch: ${result.currentBranch}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Silent success in normal mode
|
|
187
|
+
if (process.env.DEBUG) {
|
|
188
|
+
console.log(`[guard-main-branch] OK: Branch '${result.currentBranch}' is not protected`);
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.error(`Error: ${result.error}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Run main if executed directly
|
|
199
|
+
import { runCLI } from './cli-entry-point.js';
|
|
200
|
+
if (import.meta.main) {
|
|
201
|
+
runCLI(main);
|
|
202
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file guard-worktree-commit.ts
|
|
4
|
+
* @description Guard that prevents WU commits from main checkout (WU-1111)
|
|
5
|
+
*
|
|
6
|
+
* Validates that WU-related commits are only made from worktrees, not main.
|
|
7
|
+
* Used by git commit-msg hooks to enforce worktree discipline.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* guard-worktree-commit "commit message"
|
|
11
|
+
* guard-worktree-commit --message "commit message"
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - Commit allowed
|
|
15
|
+
* 1 - Commit blocked (WU commit from main)
|
|
16
|
+
*
|
|
17
|
+
* @see {@link docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md} - Worktree discipline
|
|
18
|
+
*/
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { isInWorktree, isMainBranch } from '@lumenflow/core/dist/core/worktree-guard.js';
|
|
21
|
+
const LOG_PREFIX = '[guard-worktree-commit]';
|
|
22
|
+
/**
|
|
23
|
+
* Patterns that indicate a WU-related commit message
|
|
24
|
+
*/
|
|
25
|
+
const WU_COMMIT_PATTERNS = [
|
|
26
|
+
/^wu\(/i, // wu(WU-123): message
|
|
27
|
+
/\(wu-\d+\)/i, // feat(WU-123): message
|
|
28
|
+
/\(WU-\d+\)/i, // Same with uppercase
|
|
29
|
+
/^WU-\d+:/i, // WU-123: message
|
|
30
|
+
/^wu-\d+:/i, // wu-123: message
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Check if a commit should be blocked
|
|
34
|
+
*
|
|
35
|
+
* @param options - Check options
|
|
36
|
+
* @param options.commitMessage - The commit message
|
|
37
|
+
* @param options.isMainCheckout - Whether in main checkout
|
|
38
|
+
* @param options.isInWorktree - Whether in a worktree
|
|
39
|
+
* @returns Whether commit should be blocked and why
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const result = shouldBlockCommit({
|
|
43
|
+
* commitMessage: 'wu(WU-123): add feature',
|
|
44
|
+
* isMainCheckout: true,
|
|
45
|
+
* isInWorktree: false,
|
|
46
|
+
* });
|
|
47
|
+
* if (result.blocked) {
|
|
48
|
+
* console.error(result.reason);
|
|
49
|
+
* process.exit(1);
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
export function shouldBlockCommit(options) {
|
|
53
|
+
const { commitMessage, isMainCheckout, isInWorktree } = options;
|
|
54
|
+
// Allow all commits from worktrees
|
|
55
|
+
if (isInWorktree) {
|
|
56
|
+
return { blocked: false };
|
|
57
|
+
}
|
|
58
|
+
// Check if commit message indicates WU work
|
|
59
|
+
const isWUCommit = WU_COMMIT_PATTERNS.some((pattern) => pattern.test(commitMessage));
|
|
60
|
+
// Block WU commits from main checkout
|
|
61
|
+
if (isWUCommit && isMainCheckout) {
|
|
62
|
+
return {
|
|
63
|
+
blocked: true,
|
|
64
|
+
reason: `${LOG_PREFIX} BLOCKED: WU commits must be made from a worktree.
|
|
65
|
+
|
|
66
|
+
You are attempting to commit WU work from the main checkout.
|
|
67
|
+
|
|
68
|
+
To fix:
|
|
69
|
+
1. Navigate to your worktree:
|
|
70
|
+
cd worktrees/<lane>-wu-xxx/
|
|
71
|
+
|
|
72
|
+
2. Make your commit there:
|
|
73
|
+
git add . && git commit -m "${commitMessage}"
|
|
74
|
+
|
|
75
|
+
3. Complete the WU from main:
|
|
76
|
+
cd ../.. && pnpm wu:done --id WU-XXX
|
|
77
|
+
|
|
78
|
+
For more information:
|
|
79
|
+
See docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md
|
|
80
|
+
See .claude/skills/worktree-discipline/SKILL.md
|
|
81
|
+
`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Allow non-WU commits from anywhere
|
|
85
|
+
return { blocked: false };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if a commit message is WU-related
|
|
89
|
+
*
|
|
90
|
+
* @param message - Commit message to check
|
|
91
|
+
* @returns true if message indicates WU work
|
|
92
|
+
*/
|
|
93
|
+
export function isWUCommitMessage(message) {
|
|
94
|
+
return WU_COMMIT_PATTERNS.some((pattern) => pattern.test(message));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Main CLI entry point
|
|
98
|
+
*/
|
|
99
|
+
async function main() {
|
|
100
|
+
const args = process.argv.slice(2);
|
|
101
|
+
// Parse arguments
|
|
102
|
+
let commitMessage;
|
|
103
|
+
for (let i = 0; i < args.length; i++) {
|
|
104
|
+
const arg = args[i];
|
|
105
|
+
if (arg === '--message' || arg === '-m') {
|
|
106
|
+
commitMessage = args[++i];
|
|
107
|
+
}
|
|
108
|
+
else if (arg === '--help' || arg === '-h') {
|
|
109
|
+
console.log(`Usage: guard-worktree-commit [--message] "commit message"
|
|
110
|
+
|
|
111
|
+
Check if a WU commit should be blocked from main checkout.
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
--message, -m MSG Commit message to check
|
|
115
|
+
-h, --help Show this help message
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
guard-worktree-commit "wu(WU-123): add feature"
|
|
119
|
+
guard-worktree-commit --message "chore: update deps"
|
|
120
|
+
`);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
else if (!commitMessage) {
|
|
124
|
+
commitMessage = arg;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!commitMessage) {
|
|
128
|
+
console.error(`${LOG_PREFIX} Error: Commit message required`);
|
|
129
|
+
console.error('Usage: guard-worktree-commit [--message] "commit message"');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
// Check context
|
|
133
|
+
const inWorktree = isInWorktree();
|
|
134
|
+
let onMain = false;
|
|
135
|
+
try {
|
|
136
|
+
onMain = await isMainBranch();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// If we can't determine branch, be conservative and allow
|
|
140
|
+
onMain = false;
|
|
141
|
+
}
|
|
142
|
+
const result = shouldBlockCommit({
|
|
143
|
+
commitMessage,
|
|
144
|
+
isMainCheckout: onMain && !inWorktree,
|
|
145
|
+
isInWorktree: inWorktree,
|
|
146
|
+
});
|
|
147
|
+
if (result.blocked) {
|
|
148
|
+
console.error(result.reason);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
console.log(`${LOG_PREFIX} Commit allowed`);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
// Guard main() for testability
|
|
155
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
156
|
+
main().catch((error) => {
|
|
157
|
+
console.error(`${LOG_PREFIX} Unexpected error:`, error);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
|
160
|
+
}
|