@lumenflow/core 1.3.0 → 1.3.3
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/arg-parser.d.ts +6 -0
- package/dist/arg-parser.js +16 -0
- package/dist/core/tool.schemas.d.ts +1 -1
- package/dist/coverage-gate.d.ts +3 -0
- package/dist/coverage-gate.js +7 -4
- package/dist/force-bypass-audit.d.ts +63 -0
- package/dist/force-bypass-audit.js +140 -0
- package/dist/gates-config.d.ts +132 -0
- package/dist/gates-config.js +229 -0
- package/dist/git-adapter.d.ts +7 -0
- package/dist/git-adapter.js +15 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/lumenflow-config-schema.d.ts +97 -0
- package/dist/lumenflow-config-schema.js +9 -0
- package/dist/lumenflow-home.d.ts +130 -0
- package/dist/lumenflow-home.js +211 -0
- package/dist/manual-test-validator.d.ts +3 -0
- package/dist/manual-test-validator.js +7 -4
- package/dist/orphan-detector.d.ts +16 -0
- package/dist/orphan-detector.js +24 -0
- package/dist/prompt-linter.js +2 -1
- package/dist/prompt-monitor.js +3 -1
- package/dist/spec-branch-helpers.d.ts +118 -0
- package/dist/spec-branch-helpers.js +199 -0
- package/dist/user-normalizer.d.ts +5 -1
- package/dist/user-normalizer.js +6 -1
- package/dist/validators/phi-scanner.js +6 -0
- package/dist/worktree-symlink.d.ts +4 -0
- package/dist/worktree-symlink.js +14 -20
- package/dist/wu-consistency-checker.d.ts +2 -0
- package/dist/wu-consistency-checker.js +35 -1
- package/dist/wu-constants.d.ts +193 -0
- package/dist/wu-constants.js +200 -4
- package/dist/wu-create-validators.d.ts +57 -2
- package/dist/wu-create-validators.js +111 -2
- package/dist/wu-done-branch-only.js +9 -0
- package/dist/wu-done-docs-generate.d.ts +73 -0
- package/dist/wu-done-docs-generate.js +108 -0
- package/dist/wu-done-worktree.js +12 -0
- package/dist/wu-schema.js +3 -1
- package/dist/wu-spawn.js +15 -2
- package/dist/wu-yaml-fixer.js +6 -3
- package/package.json +12 -11
package/dist/arg-parser.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ interface WUOption {
|
|
|
21
21
|
isRepeatable?: boolean;
|
|
22
22
|
}
|
|
23
23
|
export declare const WU_OPTIONS: Record<string, WUOption>;
|
|
24
|
+
/**
|
|
25
|
+
* WU-1062: Additional options for wu:create command
|
|
26
|
+
*
|
|
27
|
+
* These options control how wu:create handles external plan storage.
|
|
28
|
+
*/
|
|
29
|
+
export declare const WU_CREATE_OPTIONS: Record<string, WUOption>;
|
|
24
30
|
/**
|
|
25
31
|
* Create a commander-based CLI parser for a WU script.
|
|
26
32
|
*
|
package/dist/arg-parser.js
CHANGED
|
@@ -396,6 +396,22 @@ export const WU_OPTIONS = {
|
|
|
396
396
|
description: 'Skip automatic pnpm install in worktree after creation (faster claims when deps already built)',
|
|
397
397
|
},
|
|
398
398
|
};
|
|
399
|
+
/**
|
|
400
|
+
* WU-1062: Additional options for wu:create command
|
|
401
|
+
*
|
|
402
|
+
* These options control how wu:create handles external plan storage.
|
|
403
|
+
*/
|
|
404
|
+
export const WU_CREATE_OPTIONS = {
|
|
405
|
+
/**
|
|
406
|
+
* Create plan template in $LUMENFLOW_HOME/plans/
|
|
407
|
+
* Stores plans externally for traceability without polluting the repo.
|
|
408
|
+
*/
|
|
409
|
+
plan: {
|
|
410
|
+
name: 'plan',
|
|
411
|
+
flags: '--plan',
|
|
412
|
+
description: 'Create plan template in $LUMENFLOW_HOME/plans/ (external plan storage)',
|
|
413
|
+
},
|
|
414
|
+
};
|
|
399
415
|
/**
|
|
400
416
|
* Negated options that commander handles specially.
|
|
401
417
|
* --no-foo creates opts.foo = false. We convert to noFoo = true.
|
|
@@ -140,8 +140,8 @@ export declare const ToolExecutionResultSchema: z.ZodObject<{
|
|
|
140
140
|
tool: z.ZodString;
|
|
141
141
|
status: z.ZodEnum<{
|
|
142
142
|
cancelled: "cancelled";
|
|
143
|
-
success: "success";
|
|
144
143
|
timeout: "timeout";
|
|
144
|
+
success: "success";
|
|
145
145
|
failed: "failed";
|
|
146
146
|
pending: "pending";
|
|
147
147
|
running: "running";
|
package/dist/coverage-gate.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export declare const COVERAGE_GATE_MODES: Readonly<{
|
|
|
26
26
|
* Glob patterns for hex core files that require ≥90% coverage.
|
|
27
27
|
* These are the critical application layer files.
|
|
28
28
|
*
|
|
29
|
+
* WU-1068: Changed from @patientpath to @lumenflow for framework reusability.
|
|
30
|
+
* Project-specific patterns should be configured in .lumenflow.config.yaml.
|
|
31
|
+
*
|
|
29
32
|
* @constant {string[]}
|
|
30
33
|
*/
|
|
31
34
|
export declare const HEX_CORE_PATTERNS: readonly string[];
|
package/dist/coverage-gate.js
CHANGED
|
@@ -29,11 +29,14 @@ export const COVERAGE_GATE_MODES = Object.freeze({
|
|
|
29
29
|
* Glob patterns for hex core files that require ≥90% coverage.
|
|
30
30
|
* These are the critical application layer files.
|
|
31
31
|
*
|
|
32
|
+
* WU-1068: Changed from @patientpath to @lumenflow for framework reusability.
|
|
33
|
+
* Project-specific patterns should be configured in .lumenflow.config.yaml.
|
|
34
|
+
*
|
|
32
35
|
* @constant {string[]}
|
|
33
36
|
*/
|
|
34
37
|
export const HEX_CORE_PATTERNS = Object.freeze([
|
|
35
|
-
'packages/@
|
|
36
|
-
'packages/@
|
|
38
|
+
'packages/@lumenflow/core/',
|
|
39
|
+
'packages/@lumenflow/cli/',
|
|
37
40
|
]);
|
|
38
41
|
/**
|
|
39
42
|
* Coverage threshold for hex core files (percentage)
|
|
@@ -144,7 +147,7 @@ export function formatCoverageDelta(coverageData) {
|
|
|
144
147
|
const metrics = metricsValue;
|
|
145
148
|
const pct = metrics.lines?.pct ?? 0;
|
|
146
149
|
const status = pct >= COVERAGE_THRESHOLD ? EMOJI.SUCCESS : EMOJI.FAILURE;
|
|
147
|
-
const shortFile = file.replace('packages/@
|
|
150
|
+
const shortFile = file.replace('packages/@lumenflow/', '');
|
|
148
151
|
lines.push(` ${status} ${shortFile}: ${pct.toFixed(1)}%`);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
@@ -178,7 +181,7 @@ export async function runCoverageGate(options = {}) {
|
|
|
178
181
|
if (!pass) {
|
|
179
182
|
logger.log(`\n${EMOJI.FAILURE} Coverage below ${COVERAGE_THRESHOLD}% for hex core files:`);
|
|
180
183
|
for (const failure of failures) {
|
|
181
|
-
const shortFile = failure.file.replace('packages/@
|
|
184
|
+
const shortFile = failure.file.replace('packages/@lumenflow/', '');
|
|
182
185
|
logger.log(` - ${shortFile}: ${failure.actual.toFixed(1)}% (requires ${failure.threshold}%)`);
|
|
183
186
|
}
|
|
184
187
|
if (mode === COVERAGE_GATE_MODES.BLOCK) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-1070: Force Bypass Audit Logging
|
|
3
|
+
*
|
|
4
|
+
* Provides audit logging for LUMENFLOW_FORCE bypass usage.
|
|
5
|
+
* All hooks log bypass events to .beacon/force-bypasses.log for accountability.
|
|
6
|
+
*
|
|
7
|
+
* Key principles:
|
|
8
|
+
* - FAIL-OPEN: Audit logging must never block the bypass itself
|
|
9
|
+
* - WARN ON MISSING REASON: stderr warning if LUMENFLOW_FORCE_REASON not set
|
|
10
|
+
* - GIT-TRACKED: .beacon/force-bypasses.log is version controlled
|
|
11
|
+
*
|
|
12
|
+
* Log format:
|
|
13
|
+
* ISO_TIMESTAMP | HOOK_NAME | GIT_USER | BRANCH | REASON | CWD
|
|
14
|
+
*
|
|
15
|
+
* Example:
|
|
16
|
+
* 2026-01-23T10:30:00.000Z | pre-commit | tom@hellm.ai | lane/ops/wu-1070 | Emergency hotfix | /project
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Environment variable names for force bypass
|
|
20
|
+
*/
|
|
21
|
+
export declare const FORCE_ENV_VAR = "LUMENFLOW_FORCE";
|
|
22
|
+
export declare const FORCE_REASON_ENV_VAR = "LUMENFLOW_FORCE_REASON";
|
|
23
|
+
/**
|
|
24
|
+
* Represents a parsed audit log entry
|
|
25
|
+
*/
|
|
26
|
+
export interface AuditLogEntry {
|
|
27
|
+
timestamp: string;
|
|
28
|
+
hook: string;
|
|
29
|
+
user: string;
|
|
30
|
+
branch: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if LUMENFLOW_FORCE bypass is active
|
|
36
|
+
*
|
|
37
|
+
* @returns true if LUMENFLOW_FORCE=1
|
|
38
|
+
*/
|
|
39
|
+
export declare function shouldBypass(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get the audit log file path
|
|
42
|
+
*
|
|
43
|
+
* @param projectRoot - Project root directory
|
|
44
|
+
* @returns Full path to the audit log file
|
|
45
|
+
*/
|
|
46
|
+
export declare function getAuditLogPath(projectRoot: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Log a force bypass event to the audit log
|
|
49
|
+
*
|
|
50
|
+
* This function is FAIL-OPEN: if logging fails, it warns to stderr
|
|
51
|
+
* but does NOT throw or block the bypass operation.
|
|
52
|
+
*
|
|
53
|
+
* @param hookName - Name of the hook being bypassed (pre-commit, pre-push, etc.)
|
|
54
|
+
* @param projectRoot - Project root directory
|
|
55
|
+
*/
|
|
56
|
+
export declare function logForceBypass(hookName: string, projectRoot: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Parse a single line from the audit log
|
|
59
|
+
*
|
|
60
|
+
* @param line - Raw log line
|
|
61
|
+
* @returns Parsed entry or null if invalid
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseAuditLogLine(line: string): AuditLogEntry | null;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-1070: Force Bypass Audit Logging
|
|
3
|
+
*
|
|
4
|
+
* Provides audit logging for LUMENFLOW_FORCE bypass usage.
|
|
5
|
+
* All hooks log bypass events to .beacon/force-bypasses.log for accountability.
|
|
6
|
+
*
|
|
7
|
+
* Key principles:
|
|
8
|
+
* - FAIL-OPEN: Audit logging must never block the bypass itself
|
|
9
|
+
* - WARN ON MISSING REASON: stderr warning if LUMENFLOW_FORCE_REASON not set
|
|
10
|
+
* - GIT-TRACKED: .beacon/force-bypasses.log is version controlled
|
|
11
|
+
*
|
|
12
|
+
* Log format:
|
|
13
|
+
* ISO_TIMESTAMP | HOOK_NAME | GIT_USER | BRANCH | REASON | CWD
|
|
14
|
+
*
|
|
15
|
+
* Example:
|
|
16
|
+
* 2026-01-23T10:30:00.000Z | pre-commit | tom@hellm.ai | lane/ops/wu-1070 | Emergency hotfix | /project
|
|
17
|
+
*/
|
|
18
|
+
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
/**
|
|
22
|
+
* Environment variable names for force bypass
|
|
23
|
+
*/
|
|
24
|
+
export const FORCE_ENV_VAR = 'LUMENFLOW_FORCE';
|
|
25
|
+
export const FORCE_REASON_ENV_VAR = 'LUMENFLOW_FORCE_REASON';
|
|
26
|
+
/**
|
|
27
|
+
* Log file path relative to project root
|
|
28
|
+
*/
|
|
29
|
+
const LOG_FILE_NAME = '.beacon/force-bypasses.log';
|
|
30
|
+
/**
|
|
31
|
+
* Check if LUMENFLOW_FORCE bypass is active
|
|
32
|
+
*
|
|
33
|
+
* @returns true if LUMENFLOW_FORCE=1
|
|
34
|
+
*/
|
|
35
|
+
export function shouldBypass() {
|
|
36
|
+
return process.env[FORCE_ENV_VAR] === '1';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the audit log file path
|
|
40
|
+
*
|
|
41
|
+
* @param projectRoot - Project root directory
|
|
42
|
+
* @returns Full path to the audit log file
|
|
43
|
+
*/
|
|
44
|
+
export function getAuditLogPath(projectRoot) {
|
|
45
|
+
return join(projectRoot, LOG_FILE_NAME);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get git user.name for audit logging
|
|
49
|
+
*
|
|
50
|
+
* @returns Git user name or 'unknown' if not configured
|
|
51
|
+
*/
|
|
52
|
+
function getGitUser() {
|
|
53
|
+
try {
|
|
54
|
+
return execSync('git config user.name', { encoding: 'utf8' }).trim();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get current git branch name
|
|
62
|
+
*
|
|
63
|
+
* @returns Branch name or 'unknown' if not in a git repo
|
|
64
|
+
*/
|
|
65
|
+
function getGitBranch() {
|
|
66
|
+
try {
|
|
67
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Log a force bypass event to the audit log
|
|
75
|
+
*
|
|
76
|
+
* This function is FAIL-OPEN: if logging fails, it warns to stderr
|
|
77
|
+
* but does NOT throw or block the bypass operation.
|
|
78
|
+
*
|
|
79
|
+
* @param hookName - Name of the hook being bypassed (pre-commit, pre-push, etc.)
|
|
80
|
+
* @param projectRoot - Project root directory
|
|
81
|
+
*/
|
|
82
|
+
export function logForceBypass(hookName, projectRoot) {
|
|
83
|
+
// Only log when bypass is active
|
|
84
|
+
if (!shouldBypass()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Get reason from environment (warn if missing)
|
|
88
|
+
const reason = process.env[FORCE_REASON_ENV_VAR];
|
|
89
|
+
if (!reason) {
|
|
90
|
+
console.warn(`[${hookName}] Warning: ${FORCE_REASON_ENV_VAR} not set. ` +
|
|
91
|
+
'Consider providing a reason for the bypass: ' +
|
|
92
|
+
`${FORCE_REASON_ENV_VAR}="reason here" LUMENFLOW_FORCE=1 git ...`);
|
|
93
|
+
}
|
|
94
|
+
// Collect audit data
|
|
95
|
+
const timestamp = new Date().toISOString();
|
|
96
|
+
const user = getGitUser();
|
|
97
|
+
const branch = getGitBranch();
|
|
98
|
+
const cwd = projectRoot;
|
|
99
|
+
const reasonText = reason || '(no reason provided)';
|
|
100
|
+
// Format log line: ISO timestamp | hook | user | branch | reason | cwd
|
|
101
|
+
const logLine = `${timestamp} | ${hookName} | ${user} | ${branch} | ${reasonText} | ${cwd}\n`;
|
|
102
|
+
// Write to log file (fail-open)
|
|
103
|
+
try {
|
|
104
|
+
const logPath = getAuditLogPath(projectRoot);
|
|
105
|
+
const beaconDir = join(projectRoot, '.beacon');
|
|
106
|
+
// Ensure .beacon directory exists
|
|
107
|
+
if (!existsSync(beaconDir)) {
|
|
108
|
+
mkdirSync(beaconDir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
appendFileSync(logPath, logLine);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// Fail-open: warn but don't block
|
|
114
|
+
console.error(`[${hookName}] Warning: Failed to write to audit log: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse a single line from the audit log
|
|
119
|
+
*
|
|
120
|
+
* @param line - Raw log line
|
|
121
|
+
* @returns Parsed entry or null if invalid
|
|
122
|
+
*/
|
|
123
|
+
export function parseAuditLogLine(line) {
|
|
124
|
+
if (!line || line.trim() === '') {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const parts = line.split(' | ');
|
|
128
|
+
if (parts.length !== 6) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const [timestamp, hook, user, branch, reason, cwd] = parts;
|
|
132
|
+
return {
|
|
133
|
+
timestamp: timestamp.trim(),
|
|
134
|
+
hook: hook.trim(),
|
|
135
|
+
user: user.trim(),
|
|
136
|
+
branch: branch.trim(),
|
|
137
|
+
reason: reason.trim(),
|
|
138
|
+
cwd: cwd.trim(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gates Configuration
|
|
3
|
+
*
|
|
4
|
+
* WU-1067: Config-driven gates execution
|
|
5
|
+
*
|
|
6
|
+
* Provides a config-driven gates system that allows users to define
|
|
7
|
+
* custom format, lint, test commands in .lumenflow.config.yaml instead
|
|
8
|
+
* of relying on hardcoded language presets.
|
|
9
|
+
*
|
|
10
|
+
* @module gates-config
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
/**
|
|
14
|
+
* Schema for a gate command - either a string or an object with options
|
|
15
|
+
*/
|
|
16
|
+
export declare const GateCommandConfigSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
17
|
+
command: z.ZodString;
|
|
18
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
}, z.core.$strip>]>;
|
|
21
|
+
/**
|
|
22
|
+
* Type for parsed gate command configuration
|
|
23
|
+
*/
|
|
24
|
+
export type GateCommandConfig = z.infer<typeof GateCommandConfigSchema>;
|
|
25
|
+
/**
|
|
26
|
+
* Schema for the gates execution configuration section
|
|
27
|
+
*/
|
|
28
|
+
export declare const GatesExecutionConfigSchema: z.ZodObject<{
|
|
29
|
+
preset: z.ZodOptional<z.ZodString>;
|
|
30
|
+
setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
31
|
+
command: z.ZodString;
|
|
32
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
}, z.core.$strip>]>>;
|
|
35
|
+
format: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
36
|
+
command: z.ZodString;
|
|
37
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
}, z.core.$strip>]>>;
|
|
40
|
+
lint: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
41
|
+
command: z.ZodString;
|
|
42
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
43
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
}, z.core.$strip>]>>;
|
|
45
|
+
typecheck: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
46
|
+
command: z.ZodString;
|
|
47
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
}, z.core.$strip>]>>;
|
|
50
|
+
test: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
51
|
+
command: z.ZodString;
|
|
52
|
+
continueOnError: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
}, z.core.$strip>]>>;
|
|
55
|
+
coverage: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
56
|
+
command: z.ZodString;
|
|
57
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
}, z.core.$strip>]>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
/**
|
|
61
|
+
* Type for gates execution configuration
|
|
62
|
+
*/
|
|
63
|
+
export type GatesExecutionConfig = z.infer<typeof GatesExecutionConfigSchema>;
|
|
64
|
+
/**
|
|
65
|
+
* Parsed gate command ready for execution
|
|
66
|
+
*/
|
|
67
|
+
export interface ParsedGateCommand {
|
|
68
|
+
/** The shell command to execute */
|
|
69
|
+
command: string;
|
|
70
|
+
/** Whether to continue if this gate fails */
|
|
71
|
+
continueOnError: boolean;
|
|
72
|
+
/** Timeout in milliseconds */
|
|
73
|
+
timeout: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Gate preset definitions
|
|
77
|
+
*
|
|
78
|
+
* These provide sensible defaults for common language ecosystems.
|
|
79
|
+
* Users can override any field via .lumenflow.config.yaml
|
|
80
|
+
*/
|
|
81
|
+
export declare const GATE_PRESETS: Record<string, Partial<GatesExecutionConfig>>;
|
|
82
|
+
/**
|
|
83
|
+
* Parse a gate command configuration into executable form
|
|
84
|
+
*
|
|
85
|
+
* @param config - Gate command configuration (string or object)
|
|
86
|
+
* @returns Parsed command with defaults applied, or null if undefined
|
|
87
|
+
*/
|
|
88
|
+
export declare function parseGateCommand(config: GateCommandConfig | undefined): ParsedGateCommand | null;
|
|
89
|
+
/**
|
|
90
|
+
* Expand a preset name into its default gate commands
|
|
91
|
+
*
|
|
92
|
+
* @param preset - Preset name (node, python, go, rust, dotnet) or undefined
|
|
93
|
+
* @returns Partial gates config with preset defaults, or empty object if unknown
|
|
94
|
+
*/
|
|
95
|
+
export declare function expandPreset(preset: string | undefined): Partial<GatesExecutionConfig>;
|
|
96
|
+
/**
|
|
97
|
+
* Load gates configuration from .lumenflow.config.yaml
|
|
98
|
+
*
|
|
99
|
+
* @param projectRoot - Project root directory
|
|
100
|
+
* @returns Gates execution config, or null if not configured
|
|
101
|
+
*/
|
|
102
|
+
export declare function loadGatesConfig(projectRoot: string): GatesExecutionConfig | null;
|
|
103
|
+
/**
|
|
104
|
+
* Get default gates configuration for auto-detection fallback
|
|
105
|
+
*
|
|
106
|
+
* Used when no gates config is present in .lumenflow.config.yaml.
|
|
107
|
+
* These are generic commands that work across common setups.
|
|
108
|
+
*
|
|
109
|
+
* @returns Default gates configuration
|
|
110
|
+
*/
|
|
111
|
+
export declare function getDefaultGatesConfig(): GatesExecutionConfig;
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the effective gates configuration
|
|
114
|
+
*
|
|
115
|
+
* Priority order:
|
|
116
|
+
* 1. Explicit config from .lumenflow.config.yaml
|
|
117
|
+
* 2. Preset defaults (if preset specified)
|
|
118
|
+
* 3. Auto-detection defaults
|
|
119
|
+
*
|
|
120
|
+
* @param projectRoot - Project root directory
|
|
121
|
+
* @returns Resolved gates configuration
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveGatesConfig(projectRoot: string): GatesExecutionConfig;
|
|
124
|
+
/**
|
|
125
|
+
* Check if a specific gate should be skipped
|
|
126
|
+
*
|
|
127
|
+
* @param gateName - Name of the gate (format, lint, typecheck, test)
|
|
128
|
+
* @param config - Gates execution configuration
|
|
129
|
+
* @param skipFlags - Map of skip flags from CLI/Action inputs
|
|
130
|
+
* @returns True if the gate should be skipped
|
|
131
|
+
*/
|
|
132
|
+
export declare function shouldSkipGate(gateName: keyof Omit<GatesExecutionConfig, 'preset' | 'coverage'>, config: GatesExecutionConfig, skipFlags: Record<string, boolean>): boolean;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gates Configuration
|
|
3
|
+
*
|
|
4
|
+
* WU-1067: Config-driven gates execution
|
|
5
|
+
*
|
|
6
|
+
* Provides a config-driven gates system that allows users to define
|
|
7
|
+
* custom format, lint, test commands in .lumenflow.config.yaml instead
|
|
8
|
+
* of relying on hardcoded language presets.
|
|
9
|
+
*
|
|
10
|
+
* @module gates-config
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as yaml from 'yaml';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
/**
|
|
17
|
+
* Default timeout for gate commands (2 minutes)
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_GATE_TIMEOUT = 120000;
|
|
20
|
+
/**
|
|
21
|
+
* Schema for a gate command object with options
|
|
22
|
+
*/
|
|
23
|
+
const GateCommandObjectSchema = z.object({
|
|
24
|
+
/** The shell command to execute */
|
|
25
|
+
command: z.string(),
|
|
26
|
+
/** Whether to continue if this gate fails (default: false) */
|
|
27
|
+
continueOnError: z.boolean().optional(),
|
|
28
|
+
/** Timeout in milliseconds (default: 120000) */
|
|
29
|
+
timeout: z.number().int().positive().optional(),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Schema for a gate command - either a string or an object with options
|
|
33
|
+
*/
|
|
34
|
+
export const GateCommandConfigSchema = z.union([z.string(), GateCommandObjectSchema]);
|
|
35
|
+
/**
|
|
36
|
+
* Schema for the gates execution configuration section
|
|
37
|
+
*/
|
|
38
|
+
export const GatesExecutionConfigSchema = z.object({
|
|
39
|
+
/** Preset to use for default commands (node, python, go, rust, dotnet) */
|
|
40
|
+
preset: z.string().optional(),
|
|
41
|
+
/** Setup command (e.g., install dependencies) */
|
|
42
|
+
setup: GateCommandConfigSchema.optional(),
|
|
43
|
+
/** Format check command */
|
|
44
|
+
format: GateCommandConfigSchema.optional(),
|
|
45
|
+
/** Lint command */
|
|
46
|
+
lint: GateCommandConfigSchema.optional(),
|
|
47
|
+
/** Type check command */
|
|
48
|
+
typecheck: GateCommandConfigSchema.optional(),
|
|
49
|
+
/** Test command */
|
|
50
|
+
test: GateCommandConfigSchema.optional(),
|
|
51
|
+
/** Coverage configuration */
|
|
52
|
+
coverage: z
|
|
53
|
+
.union([
|
|
54
|
+
z.string(),
|
|
55
|
+
z.object({
|
|
56
|
+
command: z.string(),
|
|
57
|
+
threshold: z.number().min(0).max(100).optional(),
|
|
58
|
+
}),
|
|
59
|
+
])
|
|
60
|
+
.optional(),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Gate preset definitions
|
|
64
|
+
*
|
|
65
|
+
* These provide sensible defaults for common language ecosystems.
|
|
66
|
+
* Users can override any field via .lumenflow.config.yaml
|
|
67
|
+
*/
|
|
68
|
+
export const GATE_PRESETS = {
|
|
69
|
+
node: {
|
|
70
|
+
setup: 'npm ci || npm install',
|
|
71
|
+
format: 'npx prettier --check .',
|
|
72
|
+
lint: 'npx eslint .',
|
|
73
|
+
typecheck: 'npx tsc --noEmit',
|
|
74
|
+
test: 'npm test',
|
|
75
|
+
},
|
|
76
|
+
python: {
|
|
77
|
+
setup: 'pip install -e ".[dev]" || pip install -r requirements.txt',
|
|
78
|
+
format: 'ruff format --check .',
|
|
79
|
+
lint: 'ruff check .',
|
|
80
|
+
typecheck: 'mypy .',
|
|
81
|
+
test: 'pytest',
|
|
82
|
+
},
|
|
83
|
+
go: {
|
|
84
|
+
format: 'gofmt -l . | grep -v "^$" && exit 1 || exit 0',
|
|
85
|
+
lint: 'golangci-lint run',
|
|
86
|
+
typecheck: 'go vet ./...',
|
|
87
|
+
test: 'go test ./...',
|
|
88
|
+
},
|
|
89
|
+
rust: {
|
|
90
|
+
format: 'cargo fmt --check',
|
|
91
|
+
lint: 'cargo clippy -- -D warnings',
|
|
92
|
+
typecheck: 'cargo check',
|
|
93
|
+
test: 'cargo test',
|
|
94
|
+
},
|
|
95
|
+
dotnet: {
|
|
96
|
+
setup: 'dotnet restore',
|
|
97
|
+
format: 'dotnet format --verify-no-changes',
|
|
98
|
+
lint: 'dotnet build --no-restore -warnaserror',
|
|
99
|
+
test: 'dotnet test --no-restore',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Parse a gate command configuration into executable form
|
|
104
|
+
*
|
|
105
|
+
* @param config - Gate command configuration (string or object)
|
|
106
|
+
* @returns Parsed command with defaults applied, or null if undefined
|
|
107
|
+
*/
|
|
108
|
+
export function parseGateCommand(config) {
|
|
109
|
+
if (config === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (typeof config === 'string') {
|
|
113
|
+
return {
|
|
114
|
+
command: config,
|
|
115
|
+
continueOnError: false,
|
|
116
|
+
timeout: DEFAULT_GATE_TIMEOUT,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
command: config.command,
|
|
121
|
+
continueOnError: config.continueOnError ?? false,
|
|
122
|
+
timeout: config.timeout ?? DEFAULT_GATE_TIMEOUT,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Expand a preset name into its default gate commands
|
|
127
|
+
*
|
|
128
|
+
* @param preset - Preset name (node, python, go, rust, dotnet) or undefined
|
|
129
|
+
* @returns Partial gates config with preset defaults, or empty object if unknown
|
|
130
|
+
*/
|
|
131
|
+
export function expandPreset(preset) {
|
|
132
|
+
if (!preset) {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
return GATE_PRESETS[preset] ?? {};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Load gates configuration from .lumenflow.config.yaml
|
|
139
|
+
*
|
|
140
|
+
* @param projectRoot - Project root directory
|
|
141
|
+
* @returns Gates execution config, or null if not configured
|
|
142
|
+
*/
|
|
143
|
+
export function loadGatesConfig(projectRoot) {
|
|
144
|
+
const configPath = path.join(projectRoot, '.lumenflow.config.yaml');
|
|
145
|
+
if (!fs.existsSync(configPath)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
150
|
+
const data = yaml.parse(content);
|
|
151
|
+
// Check if gates.execution section exists
|
|
152
|
+
const executionConfig = data?.gates?.execution;
|
|
153
|
+
if (!executionConfig) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// Validate the config
|
|
157
|
+
const result = GatesExecutionConfigSchema.safeParse(executionConfig);
|
|
158
|
+
if (!result.success) {
|
|
159
|
+
console.warn('Warning: Invalid gates.execution config:', result.error.message);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
// Expand preset and merge with explicit config (explicit wins)
|
|
163
|
+
const presetDefaults = expandPreset(result.data.preset);
|
|
164
|
+
const merged = {
|
|
165
|
+
...presetDefaults,
|
|
166
|
+
...result.data,
|
|
167
|
+
};
|
|
168
|
+
return merged;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.warn('Warning: Failed to parse .lumenflow.config.yaml:', error instanceof Error ? error.message : String(error));
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get default gates configuration for auto-detection fallback
|
|
177
|
+
*
|
|
178
|
+
* Used when no gates config is present in .lumenflow.config.yaml.
|
|
179
|
+
* These are generic commands that work across common setups.
|
|
180
|
+
*
|
|
181
|
+
* @returns Default gates configuration
|
|
182
|
+
*/
|
|
183
|
+
export function getDefaultGatesConfig() {
|
|
184
|
+
return {
|
|
185
|
+
format: 'npm run format:check 2>/dev/null || npx prettier --check . 2>/dev/null || echo "No formatter configured"',
|
|
186
|
+
lint: 'npm run lint 2>/dev/null || npx eslint . 2>/dev/null || echo "No linter configured"',
|
|
187
|
+
typecheck: 'npm run typecheck 2>/dev/null || npx tsc --noEmit 2>/dev/null || echo "No type checker configured"',
|
|
188
|
+
test: 'npm test 2>/dev/null || echo "No test command configured"',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the effective gates configuration
|
|
193
|
+
*
|
|
194
|
+
* Priority order:
|
|
195
|
+
* 1. Explicit config from .lumenflow.config.yaml
|
|
196
|
+
* 2. Preset defaults (if preset specified)
|
|
197
|
+
* 3. Auto-detection defaults
|
|
198
|
+
*
|
|
199
|
+
* @param projectRoot - Project root directory
|
|
200
|
+
* @returns Resolved gates configuration
|
|
201
|
+
*/
|
|
202
|
+
export function resolveGatesConfig(projectRoot) {
|
|
203
|
+
const config = loadGatesConfig(projectRoot);
|
|
204
|
+
if (config) {
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
// Fall back to defaults for auto-detection
|
|
208
|
+
return getDefaultGatesConfig();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if a specific gate should be skipped
|
|
212
|
+
*
|
|
213
|
+
* @param gateName - Name of the gate (format, lint, typecheck, test)
|
|
214
|
+
* @param config - Gates execution configuration
|
|
215
|
+
* @param skipFlags - Map of skip flags from CLI/Action inputs
|
|
216
|
+
* @returns True if the gate should be skipped
|
|
217
|
+
*/
|
|
218
|
+
export function shouldSkipGate(gateName, config, skipFlags) {
|
|
219
|
+
// Check if skip flag is set
|
|
220
|
+
const skipFlagName = `skip-${gateName}`;
|
|
221
|
+
if (skipFlags[skipFlagName] || skipFlags[gateName]) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
// Check if gate is configured (undefined means skip)
|
|
225
|
+
if (config[gateName] === undefined) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
package/dist/git-adapter.d.ts
CHANGED
|
@@ -66,6 +66,13 @@ export declare class GitAdapter {
|
|
|
66
66
|
* await git.getStatus(); // " M file.txt\n?? untracked.txt"
|
|
67
67
|
*/
|
|
68
68
|
getStatus(): Promise<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Get unpushed commits (compared to upstream)
|
|
71
|
+
* @returns {Promise<string>} Oneline log output for unpushed commits
|
|
72
|
+
* @example
|
|
73
|
+
* await git.getUnpushedCommits(); // "abc123 fix: ...\n"
|
|
74
|
+
*/
|
|
75
|
+
getUnpushedCommits(): Promise<string>;
|
|
69
76
|
/**
|
|
70
77
|
* Check if a branch exists
|
|
71
78
|
* @param {string} branch - Branch name
|