@lumenflow/core 1.4.0 → 1.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/README.md CHANGED
@@ -70,23 +70,54 @@ await worktrees.remove('worktrees/operations-wu-123');
70
70
 
71
71
  ### Agent Branch Patterns
72
72
 
73
- Check if a branch is an agent branch that can bypass worktree requirements. Patterns are fetched from a central registry with 7-day caching.
73
+ Check if a branch is an agent branch that can bypass worktree requirements. Patterns are fetched from a central registry with 7-day caching, and can be configured via `.lumenflow.config.yaml`.
74
74
 
75
75
  ```typescript
76
- import { isAgentBranch, getAgentPatterns } from '@lumenflow/core';
76
+ import { isAgentBranch, isAgentBranchWithDetails, resolveAgentPatterns } from '@lumenflow/core';
77
77
 
78
78
  // Check if branch can bypass worktree requirements (async, uses registry)
79
79
  if (await isAgentBranch('claude/session-12345')) {
80
80
  console.log('Agent branch - bypass allowed');
81
81
  }
82
82
 
83
- // Get the current list of agent patterns
84
- const patterns = await getAgentPatterns();
85
- // ['agent/*', 'claude/*', 'codex/*', 'copilot/*', 'cursor/*', ...]
83
+ // Get detailed result for observability
84
+ const result = await isAgentBranchWithDetails('claude/session-123');
85
+ if (result.isMatch) {
86
+ console.log(`Matched via ${result.patternResult.source}`); // 'registry', 'merged', 'override', 'config', 'defaults'
87
+ console.log(`Registry fetched: ${result.patternResult.registryFetched}`);
88
+ }
89
+
90
+ // Resolve patterns with custom options (useful for testing)
91
+ const resolved = await resolveAgentPatterns({
92
+ configPatterns: ['my-agent/*'], // Merge with registry
93
+ // overridePatterns: ['only-this/*'], // Replace registry entirely
94
+ // disableAgentPatternRegistry: true, // Airgapped mode
95
+ });
96
+ console.log(resolved.patterns, resolved.source);
86
97
 
87
98
  // Synchronous version (uses local config only, no registry fetch)
88
99
  import { isAgentBranchSync } from '@lumenflow/core';
89
- const result = isAgentBranchSync('agent/task-123');
100
+ const syncResult = isAgentBranchSync('agent/task-123');
101
+ ```
102
+
103
+ #### Configuration Options
104
+
105
+ In `.lumenflow.config.yaml`:
106
+
107
+ ```yaml
108
+ git:
109
+ # Patterns to MERGE with registry (default: [])
110
+ agentBranchPatterns:
111
+ - 'my-custom-agent/*'
112
+ - 'internal-tool/*'
113
+
114
+ # Patterns that REPLACE registry entirely (optional)
115
+ # agentBranchPatternsOverride:
116
+ # - 'claude/*'
117
+ # - 'codex/*'
118
+
119
+ # Disable registry fetch for airgapped environments (default: false)
120
+ # disableAgentPatternRegistry: true
90
121
  ```
91
122
 
92
123
  Protected branches (main, master, lane/\*) are **never** bypassed, regardless of patterns.
@@ -5,6 +5,8 @@
5
5
  * that bypass worktree requirements. Static JSON served from lumenflow.dev,
6
6
  * cached locally for 7 days with fallback to defaults.
7
7
  *
8
+ * WU-1089: Added merge/override/airgapped modes via resolveAgentPatterns()
9
+ *
8
10
  * @module agent-patterns-registry
9
11
  */
10
12
  /** Default agent branch patterns (narrow: just agent/*) */
@@ -16,12 +18,48 @@ export declare const CACHE_TTL_MS: number;
16
18
  /**
17
19
  * Options for getAgentPatterns
18
20
  */
19
- interface GetAgentPatternsOptions {
21
+ export interface GetAgentPatternsOptions {
20
22
  /** Override cache directory (default: ~/.lumenflow/cache) */
21
23
  cacheDir?: string;
22
24
  /** Fetch timeout in milliseconds (default: 5000) */
23
25
  timeoutMs?: number;
24
26
  }
27
+ /**
28
+ * Source of agent patterns for observability
29
+ */
30
+ export type AgentPatternSource = 'registry' | 'merged' | 'override' | 'config' | 'defaults';
31
+ /**
32
+ * Result of resolveAgentPatterns with observability fields
33
+ */
34
+ export interface AgentPatternResult {
35
+ /** Resolved patterns to use for agent branch matching */
36
+ patterns: string[];
37
+ /** Source of the patterns for observability */
38
+ source: AgentPatternSource;
39
+ /** Whether the registry was successfully fetched */
40
+ registryFetched: boolean;
41
+ }
42
+ /**
43
+ * Type for injectable registry fetcher function
44
+ */
45
+ export type RegistryFetcher = (options: GetAgentPatternsOptions) => Promise<string[]>;
46
+ /**
47
+ * Options for resolveAgentPatterns (WU-1089)
48
+ */
49
+ export interface ResolveAgentPatternsOptions {
50
+ /** Injectable registry fetcher for testing (uses getAgentPatterns by default) */
51
+ registryFetcher?: RegistryFetcher;
52
+ /** Patterns from config.git.agentBranchPatterns (merged with registry by default) */
53
+ configPatterns?: string[];
54
+ /** Patterns from config.git.agentBranchPatternsOverride (replaces everything if set) */
55
+ overridePatterns?: string[];
56
+ /** config.git.disableAgentPatternRegistry - skips network fetch (airgapped mode) */
57
+ disableAgentPatternRegistry?: boolean;
58
+ /** Override cache directory (passed to fetcher) */
59
+ cacheDir?: string;
60
+ /** Fetch timeout in milliseconds (passed to fetcher) */
61
+ timeoutMs?: number;
62
+ }
25
63
  /**
26
64
  * Get the default cache directory
27
65
  *
@@ -44,10 +82,70 @@ export declare function getCacheDir(): string;
44
82
  * ```
45
83
  */
46
84
  export declare function getAgentPatterns(options?: GetAgentPatternsOptions): Promise<string[]>;
85
+ /**
86
+ * Resolve agent branch patterns based on config with merge/override/airgapped support
87
+ *
88
+ * Behavior matrix (WU-1089):
89
+ *
90
+ * | disableRegistry | override patterns | config patterns | Result | Source |
91
+ * |-----------------|-------------------|-----------------|------------------------|---------------|
92
+ * | false | undefined | undefined/[] | registry | 'registry' |
93
+ * | false | undefined | ['custom/*'] | config + registry | 'merged' |
94
+ * | false | ['only/*'] | any | override only | 'override' |
95
+ * | true | undefined | undefined/[] | defaults | 'defaults' |
96
+ * | true | undefined | ['custom/*'] | config only | 'config' |
97
+ * | true | ['only/*'] | any | override only | 'override' |
98
+ *
99
+ * When registry fetch fails:
100
+ * - Falls back to config patterns if provided, source = 'config'
101
+ * - Falls back to defaults if no config, source = 'defaults'
102
+ *
103
+ * @param options - Resolution options
104
+ * @returns Result with patterns, source, and registryFetched flag
105
+ *
106
+ * @example Default (fetch from registry)
107
+ * ```typescript
108
+ * const result = await resolveAgentPatterns({});
109
+ * // result.patterns = ['claude/*', 'codex/*', ...] (from registry)
110
+ * // result.source = 'registry'
111
+ * // result.registryFetched = true
112
+ * ```
113
+ *
114
+ * @example Merge mode (config + registry)
115
+ * ```typescript
116
+ * const result = await resolveAgentPatterns({
117
+ * configPatterns: ['my-agent/*'],
118
+ * });
119
+ * // result.patterns = ['my-agent/*', 'claude/*', 'codex/*', ...]
120
+ * // result.source = 'merged'
121
+ * // result.registryFetched = true
122
+ * ```
123
+ *
124
+ * @example Override mode (explicit replacement)
125
+ * ```typescript
126
+ * const result = await resolveAgentPatterns({
127
+ * overridePatterns: ['only-this/*'],
128
+ * });
129
+ * // result.patterns = ['only-this/*']
130
+ * // result.source = 'override'
131
+ * // result.registryFetched = false
132
+ * ```
133
+ *
134
+ * @example Airgapped mode (no network)
135
+ * ```typescript
136
+ * const result = await resolveAgentPatterns({
137
+ * disableAgentPatternRegistry: true,
138
+ * configPatterns: ['my-agent/*'],
139
+ * });
140
+ * // result.patterns = ['my-agent/*']
141
+ * // result.source = 'config'
142
+ * // result.registryFetched = false
143
+ * ```
144
+ */
145
+ export declare function resolveAgentPatterns(options?: ResolveAgentPatternsOptions): Promise<AgentPatternResult>;
47
146
  /**
48
147
  * Clear the in-memory cache
49
148
  *
50
149
  * Used primarily for testing.
51
150
  */
52
151
  export declare function clearCache(): void;
53
- export {};
@@ -5,6 +5,8 @@
5
5
  * that bypass worktree requirements. Static JSON served from lumenflow.dev,
6
6
  * cached locally for 7 days with fallback to defaults.
7
7
  *
8
+ * WU-1089: Added merge/override/airgapped modes via resolveAgentPatterns()
9
+ *
8
10
  * @module agent-patterns-registry
9
11
  */
10
12
  import * as fs from 'node:fs';
@@ -179,6 +181,128 @@ export async function getAgentPatterns(options = {}) {
179
181
  // No cache, no network - use defaults
180
182
  return DEFAULT_AGENT_PATTERNS;
181
183
  }
184
+ /**
185
+ * Resolve agent branch patterns based on config with merge/override/airgapped support
186
+ *
187
+ * Behavior matrix (WU-1089):
188
+ *
189
+ * | disableRegistry | override patterns | config patterns | Result | Source |
190
+ * |-----------------|-------------------|-----------------|------------------------|---------------|
191
+ * | false | undefined | undefined/[] | registry | 'registry' |
192
+ * | false | undefined | ['custom/*'] | config + registry | 'merged' |
193
+ * | false | ['only/*'] | any | override only | 'override' |
194
+ * | true | undefined | undefined/[] | defaults | 'defaults' |
195
+ * | true | undefined | ['custom/*'] | config only | 'config' |
196
+ * | true | ['only/*'] | any | override only | 'override' |
197
+ *
198
+ * When registry fetch fails:
199
+ * - Falls back to config patterns if provided, source = 'config'
200
+ * - Falls back to defaults if no config, source = 'defaults'
201
+ *
202
+ * @param options - Resolution options
203
+ * @returns Result with patterns, source, and registryFetched flag
204
+ *
205
+ * @example Default (fetch from registry)
206
+ * ```typescript
207
+ * const result = await resolveAgentPatterns({});
208
+ * // result.patterns = ['claude/*', 'codex/*', ...] (from registry)
209
+ * // result.source = 'registry'
210
+ * // result.registryFetched = true
211
+ * ```
212
+ *
213
+ * @example Merge mode (config + registry)
214
+ * ```typescript
215
+ * const result = await resolveAgentPatterns({
216
+ * configPatterns: ['my-agent/*'],
217
+ * });
218
+ * // result.patterns = ['my-agent/*', 'claude/*', 'codex/*', ...]
219
+ * // result.source = 'merged'
220
+ * // result.registryFetched = true
221
+ * ```
222
+ *
223
+ * @example Override mode (explicit replacement)
224
+ * ```typescript
225
+ * const result = await resolveAgentPatterns({
226
+ * overridePatterns: ['only-this/*'],
227
+ * });
228
+ * // result.patterns = ['only-this/*']
229
+ * // result.source = 'override'
230
+ * // result.registryFetched = false
231
+ * ```
232
+ *
233
+ * @example Airgapped mode (no network)
234
+ * ```typescript
235
+ * const result = await resolveAgentPatterns({
236
+ * disableAgentPatternRegistry: true,
237
+ * configPatterns: ['my-agent/*'],
238
+ * });
239
+ * // result.patterns = ['my-agent/*']
240
+ * // result.source = 'config'
241
+ * // result.registryFetched = false
242
+ * ```
243
+ */
244
+ export async function resolveAgentPatterns(options = {}) {
245
+ const { registryFetcher = getAgentPatterns, configPatterns, overridePatterns, disableAgentPatternRegistry = false, cacheDir, timeoutMs, } = options;
246
+ // Scenario 3/6: Override mode - overridePatterns replaces everything
247
+ if (overridePatterns && overridePatterns.length > 0) {
248
+ return {
249
+ patterns: overridePatterns,
250
+ source: 'override',
251
+ registryFetched: false,
252
+ };
253
+ }
254
+ // Scenario 4/5: Airgapped mode - disableAgentPatternRegistry skips network
255
+ if (disableAgentPatternRegistry) {
256
+ const hasConfigPatterns = configPatterns && configPatterns.length > 0;
257
+ return {
258
+ patterns: hasConfigPatterns ? configPatterns : DEFAULT_AGENT_PATTERNS,
259
+ source: hasConfigPatterns ? 'config' : 'defaults',
260
+ registryFetched: false,
261
+ };
262
+ }
263
+ // Scenario 1/2: Normal mode - fetch from registry, optionally merge with config
264
+ const hasConfigPatterns = configPatterns && configPatterns.length > 0;
265
+ // Try to fetch from registry
266
+ let registryPatterns = null;
267
+ let fetchedSuccessfully = false;
268
+ try {
269
+ registryPatterns = await registryFetcher({ cacheDir, timeoutMs });
270
+ fetchedSuccessfully = registryPatterns !== null && registryPatterns.length > 0;
271
+ }
272
+ catch {
273
+ // Fetch failed - will use fallback below
274
+ fetchedSuccessfully = false;
275
+ }
276
+ // If registry fetch succeeded
277
+ if (fetchedSuccessfully && registryPatterns) {
278
+ if (hasConfigPatterns) {
279
+ // Scenario 2: Merge mode - config first, then registry (deduplicated)
280
+ const merged = [...configPatterns];
281
+ for (const pattern of registryPatterns) {
282
+ if (!merged.includes(pattern)) {
283
+ merged.push(pattern);
284
+ }
285
+ }
286
+ return {
287
+ patterns: merged,
288
+ source: 'merged',
289
+ registryFetched: true,
290
+ };
291
+ }
292
+ // Scenario 1: Registry only
293
+ return {
294
+ patterns: registryPatterns,
295
+ source: 'registry',
296
+ registryFetched: true,
297
+ };
298
+ }
299
+ // Registry fetch failed - fallback to config or defaults
300
+ return {
301
+ patterns: hasConfigPatterns ? configPatterns : DEFAULT_AGENT_PATTERNS,
302
+ source: hasConfigPatterns ? 'config' : 'defaults',
303
+ registryFetched: false,
304
+ };
305
+ }
182
306
  /**
183
307
  * Clear the in-memory cache
184
308
  *
@@ -247,6 +247,13 @@ export const WU_OPTIONS = {
247
247
  flags: '--color',
248
248
  description: 'Enable colored output',
249
249
  },
250
+ // WU-1085: NO_COLOR standard support (https://no-color.org/)
251
+ noColor: {
252
+ name: 'noColor',
253
+ flags: '--no-color',
254
+ description: 'Disable colored output (respects NO_COLOR env var)',
255
+ isNegated: true,
256
+ },
250
257
  status: {
251
258
  name: 'status',
252
259
  flags: '--status <status>',
@@ -4,14 +4,19 @@
4
4
  * Provides functions to check if a branch is an agent branch that can
5
5
  * bypass worktree requirements, and if headless mode is allowed.
6
6
  *
7
+ * WU-1089: Updated to use resolveAgentPatterns with merge/override/airgapped support.
8
+ *
7
9
  * @module branch-check
8
10
  */
11
+ import { type AgentPatternResult } from './agent-patterns-registry.js';
9
12
  /**
10
13
  * Check if branch is an agent branch that can bypass worktree requirements.
11
14
  *
12
- * Uses the central registry for agent patterns (fetched from lumenflow.dev
13
- * with 7-day cache), falling back to config patterns if specified, then
14
- * to defaults.
15
+ * WU-1089: Now uses resolveAgentPatterns with proper merge/override/airgapped support:
16
+ * - Default: Fetches from registry (lumenflow.dev) with 7-day cache
17
+ * - Config patterns merge with registry patterns (config first)
18
+ * - Override patterns (agentBranchPatternsOverride) replace everything
19
+ * - Airgapped mode (disableAgentPatternRegistry) skips network fetch
15
20
  *
16
21
  * @param branch - Branch name to check
17
22
  * @returns Promise<true> if branch matches agent patterns
@@ -24,12 +29,36 @@
24
29
  * ```
25
30
  */
26
31
  export declare function isAgentBranch(branch: string | null | undefined): Promise<boolean>;
32
+ /**
33
+ * Check if branch is an agent branch with full result details.
34
+ *
35
+ * Same as isAgentBranch but returns the full AgentPatternResult
36
+ * for observability and debugging.
37
+ *
38
+ * @param branch - Branch name to check
39
+ * @returns Promise with match result and pattern resolution details
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const result = await isAgentBranchWithDetails('claude/session-123');
44
+ * if (result.isMatch) {
45
+ * console.log(`Matched via ${result.patternResult.source}`);
46
+ * console.log(`Registry fetched: ${result.patternResult.registryFetched}`);
47
+ * }
48
+ * ```
49
+ */
50
+ export declare function isAgentBranchWithDetails(branch: string | null | undefined): Promise<{
51
+ isMatch: boolean;
52
+ patternResult: AgentPatternResult;
53
+ }>;
27
54
  /**
28
55
  * Synchronous version of isAgentBranch for backwards compatibility.
29
56
  *
30
57
  * Uses only local config patterns or defaults - does NOT fetch from registry.
31
58
  * Prefer async isAgentBranch() when possible.
32
59
  *
60
+ * WU-1089: Updated to respect override and disable flags, but cannot fetch from registry.
61
+ *
33
62
  * @param branch - Branch name to check
34
63
  * @returns True if branch matches agent patterns
35
64
  *
@@ -4,11 +4,13 @@
4
4
  * Provides functions to check if a branch is an agent branch that can
5
5
  * bypass worktree requirements, and if headless mode is allowed.
6
6
  *
7
+ * WU-1089: Updated to use resolveAgentPatterns with merge/override/airgapped support.
8
+ *
7
9
  * @module branch-check
8
10
  */
9
11
  import micromatch from 'micromatch';
10
12
  import { getConfig } from './lumenflow-config.js';
11
- import { getAgentPatterns, DEFAULT_AGENT_PATTERNS } from './agent-patterns-registry.js';
13
+ import { resolveAgentPatterns, DEFAULT_AGENT_PATTERNS, } from './agent-patterns-registry.js';
12
14
  /** Legacy protected branch (always protected regardless of mainBranch setting) */
13
15
  const LEGACY_PROTECTED = 'master';
14
16
  /**
@@ -36,9 +38,11 @@ function getProtectedBranches() {
36
38
  /**
37
39
  * Check if branch is an agent branch that can bypass worktree requirements.
38
40
  *
39
- * Uses the central registry for agent patterns (fetched from lumenflow.dev
40
- * with 7-day cache), falling back to config patterns if specified, then
41
- * to defaults.
41
+ * WU-1089: Now uses resolveAgentPatterns with proper merge/override/airgapped support:
42
+ * - Default: Fetches from registry (lumenflow.dev) with 7-day cache
43
+ * - Config patterns merge with registry patterns (config first)
44
+ * - Override patterns (agentBranchPatternsOverride) replace everything
45
+ * - Airgapped mode (disableAgentPatternRegistry) skips network fetch
42
46
  *
43
47
  * @param branch - Branch name to check
44
48
  * @returns Promise<true> if branch matches agent patterns
@@ -66,18 +70,73 @@ export async function isAgentBranch(branch) {
66
70
  // LumenFlow lane branches require worktrees (uses config's laneBranchPrefix)
67
71
  if (getLaneBranchPattern().test(branch))
68
72
  return false;
69
- // Get patterns: prefer config override, then registry, then defaults
70
- let patterns;
71
- if (config?.git?.agentBranchPatterns?.length > 0) {
72
- // Config has explicit patterns - use those
73
- patterns = config.git.agentBranchPatterns;
73
+ // WU-1089: Use resolveAgentPatterns with full merge/override/airgapped support
74
+ const result = await resolveAgentPatterns({
75
+ configPatterns: config?.git?.agentBranchPatterns,
76
+ overridePatterns: config?.git?.agentBranchPatternsOverride,
77
+ disableAgentPatternRegistry: config?.git?.disableAgentPatternRegistry,
78
+ });
79
+ // Use micromatch for proper glob matching
80
+ return micromatch.isMatch(branch, result.patterns);
81
+ }
82
+ /**
83
+ * Check if branch is an agent branch with full result details.
84
+ *
85
+ * Same as isAgentBranch but returns the full AgentPatternResult
86
+ * for observability and debugging.
87
+ *
88
+ * @param branch - Branch name to check
89
+ * @returns Promise with match result and pattern resolution details
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const result = await isAgentBranchWithDetails('claude/session-123');
94
+ * if (result.isMatch) {
95
+ * console.log(`Matched via ${result.patternResult.source}`);
96
+ * console.log(`Registry fetched: ${result.patternResult.registryFetched}`);
97
+ * }
98
+ * ```
99
+ */
100
+ export async function isAgentBranchWithDetails(branch) {
101
+ // Fail-closed: no branch = protected
102
+ if (!branch) {
103
+ return {
104
+ isMatch: false,
105
+ patternResult: { patterns: [], source: 'defaults', registryFetched: false },
106
+ };
74
107
  }
75
- else {
76
- // Fetch from registry (with caching and fallback to defaults)
77
- patterns = await getAgentPatterns();
108
+ // Detached HEAD = protected (fail-closed)
109
+ if (branch === 'HEAD') {
110
+ return {
111
+ isMatch: false,
112
+ patternResult: { patterns: [], source: 'defaults', registryFetched: false },
113
+ };
78
114
  }
79
- // Use micromatch for proper glob matching
80
- return micromatch.isMatch(branch, patterns);
115
+ // Load config
116
+ const config = getConfig();
117
+ const protectedBranches = getProtectedBranches();
118
+ // Protected branches are NEVER bypassed
119
+ if (protectedBranches.includes(branch)) {
120
+ return {
121
+ isMatch: false,
122
+ patternResult: { patterns: [], source: 'defaults', registryFetched: false },
123
+ };
124
+ }
125
+ // Lane branches require worktrees
126
+ if (getLaneBranchPattern().test(branch)) {
127
+ return {
128
+ isMatch: false,
129
+ patternResult: { patterns: [], source: 'defaults', registryFetched: false },
130
+ };
131
+ }
132
+ // Resolve patterns with full details
133
+ const patternResult = await resolveAgentPatterns({
134
+ configPatterns: config?.git?.agentBranchPatterns,
135
+ overridePatterns: config?.git?.agentBranchPatternsOverride,
136
+ disableAgentPatternRegistry: config?.git?.disableAgentPatternRegistry,
137
+ });
138
+ const isMatch = micromatch.isMatch(branch, patternResult.patterns);
139
+ return { isMatch, patternResult };
81
140
  }
82
141
  /**
83
142
  * Synchronous version of isAgentBranch for backwards compatibility.
@@ -85,6 +144,8 @@ export async function isAgentBranch(branch) {
85
144
  * Uses only local config patterns or defaults - does NOT fetch from registry.
86
145
  * Prefer async isAgentBranch() when possible.
87
146
  *
147
+ * WU-1089: Updated to respect override and disable flags, but cannot fetch from registry.
148
+ *
88
149
  * @param branch - Branch name to check
89
150
  * @returns True if branch matches agent patterns
90
151
  *
@@ -106,7 +167,12 @@ export function isAgentBranchSync(branch) {
106
167
  // LumenFlow lane branches require worktrees (uses config's laneBranchPrefix)
107
168
  if (getLaneBranchPattern().test(branch))
108
169
  return false;
109
- // Use config patterns or defaults (no registry fetch in sync version)
170
+ // WU-1089: Check override first
171
+ if (config?.git?.agentBranchPatternsOverride?.length) {
172
+ return micromatch.isMatch(branch, config.git.agentBranchPatternsOverride);
173
+ }
174
+ // Use config patterns if provided, otherwise defaults
175
+ // Note: sync version cannot fetch from registry
110
176
  const patterns = config?.git?.agentBranchPatterns?.length > 0
111
177
  ? config.git.agentBranchPatterns
112
178
  : DEFAULT_AGENT_PATTERNS;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file color-support.ts
3
+ * Color control for CLI commands (WU-1085)
4
+ *
5
+ * Respects standard environment variables and CLI flags:
6
+ * - NO_COLOR: Disable colors (https://no-color.org/)
7
+ * - FORCE_COLOR: Override color level 0-3 (chalk standard)
8
+ * - --no-color: CLI flag to disable colors
9
+ *
10
+ * @see https://no-color.org/
11
+ * @see https://github.com/chalk/chalk#supportscolor
12
+ */
13
+ /**
14
+ * Get the current color level.
15
+ * @returns Color level 0-3 (0 = no colors, 3 = full 16m colors)
16
+ */
17
+ export declare function getColorLevel(): number;
18
+ /**
19
+ * Initialize color support respecting NO_COLOR and FORCE_COLOR standards.
20
+ * Call this before any colored output.
21
+ *
22
+ * Priority order:
23
+ * 1. NO_COLOR env var (always wins, per spec)
24
+ * 2. --no-color CLI flag
25
+ * 3. FORCE_COLOR env var
26
+ * 4. Default chalk detection
27
+ *
28
+ * @param argv - Command line arguments (defaults to process.argv)
29
+ * @see https://no-color.org/
30
+ * @see https://github.com/chalk/chalk#supportscolor
31
+ */
32
+ export declare function initColorSupport(argv?: string[]): void;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file color-support.ts
3
+ * Color control for CLI commands (WU-1085)
4
+ *
5
+ * Respects standard environment variables and CLI flags:
6
+ * - NO_COLOR: Disable colors (https://no-color.org/)
7
+ * - FORCE_COLOR: Override color level 0-3 (chalk standard)
8
+ * - --no-color: CLI flag to disable colors
9
+ *
10
+ * @see https://no-color.org/
11
+ * @see https://github.com/chalk/chalk#supportscolor
12
+ */
13
+ import chalk from 'chalk';
14
+ /** Internal storage for color level (allows testing without chalk singleton issues) */
15
+ let currentColorLevel = chalk.level;
16
+ /**
17
+ * Get the current color level.
18
+ * @returns Color level 0-3 (0 = no colors, 3 = full 16m colors)
19
+ */
20
+ export function getColorLevel() {
21
+ return currentColorLevel;
22
+ }
23
+ /**
24
+ * Initialize color support respecting NO_COLOR and FORCE_COLOR standards.
25
+ * Call this before any colored output.
26
+ *
27
+ * Priority order:
28
+ * 1. NO_COLOR env var (always wins, per spec)
29
+ * 2. --no-color CLI flag
30
+ * 3. FORCE_COLOR env var
31
+ * 4. Default chalk detection
32
+ *
33
+ * @param argv - Command line arguments (defaults to process.argv)
34
+ * @see https://no-color.org/
35
+ * @see https://github.com/chalk/chalk#supportscolor
36
+ */
37
+ export function initColorSupport(argv = process.argv) {
38
+ // NO_COLOR standard (https://no-color.org/)
39
+ // "When set (to any value, including empty string), it should disable colors"
40
+ if (process.env.NO_COLOR !== undefined) {
41
+ chalk.level = 0;
42
+ currentColorLevel = 0;
43
+ return;
44
+ }
45
+ // CLI --no-color flag
46
+ if (argv.includes('--no-color')) {
47
+ chalk.level = 0;
48
+ currentColorLevel = 0;
49
+ return;
50
+ }
51
+ // FORCE_COLOR override (chalk standard)
52
+ // Values: 0 = no color, 1 = basic, 2 = 256, 3 = 16m
53
+ if (process.env.FORCE_COLOR !== undefined) {
54
+ const level = parseInt(process.env.FORCE_COLOR, 10);
55
+ if (!isNaN(level) && level >= 0 && level <= 3) {
56
+ chalk.level = level;
57
+ currentColorLevel = level;
58
+ }
59
+ // Invalid values are ignored, keep default
60
+ return;
61
+ }
62
+ // Use chalk's default detection
63
+ currentColorLevel = chalk.level;
64
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Cycle Detection Module (WU-1088)
3
+ *
4
+ * Provides cycle detection for WU dependency graphs.
5
+ * Extracted from @lumenflow/initiatives to break circular dependency.
6
+ *
7
+ * @module @lumenflow/core/cycle-detector
8
+ */
9
+ /**
10
+ * WU object interface for validation and cycle detection
11
+ * Note: This interface is shared with @lumenflow/initiatives for backward compatibility
12
+ */
13
+ export interface WUObject {
14
+ id?: string;
15
+ blocks?: string[];
16
+ blocked_by?: string[];
17
+ initiative?: string;
18
+ phase?: number;
19
+ [key: string]: unknown;
20
+ }
21
+ /**
22
+ * Result of cycle detection
23
+ */
24
+ export interface CycleResult {
25
+ hasCycle: boolean;
26
+ cycles: string[][];
27
+ }
28
+ /**
29
+ * Detects circular dependencies in WU dependency graph using DFS
30
+ *
31
+ * Uses standard cycle detection: tracks visited nodes and nodes in current
32
+ * recursion stack. If we encounter a node already in the recursion stack,
33
+ * we've found a cycle.
34
+ *
35
+ * Note: This function treats both `blocks` and `blocked_by` as edges for
36
+ * traversal. This means if WU-A blocks WU-B, and WU-B's blocked_by includes
37
+ * WU-A, following both directions will find a path back to WU-A.
38
+ *
39
+ * @param wuMap - Map of WU ID to WU object
40
+ * @returns Cycle detection result with hasCycle boolean and cycles array
41
+ *
42
+ * @example
43
+ * const wuMap = new Map([
44
+ * ['WU-001', { id: 'WU-001', blocks: ['WU-002'] }],
45
+ * ['WU-002', { id: 'WU-002', blocks: ['WU-001'] }],
46
+ * ]);
47
+ * const result = detectCycles(wuMap);
48
+ * // result.hasCycle === true
49
+ * // result.cycles contains the cycle path
50
+ */
51
+ export declare function detectCycles(wuMap: Map<string, WUObject>): CycleResult;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Cycle Detection Module (WU-1088)
3
+ *
4
+ * Provides cycle detection for WU dependency graphs.
5
+ * Extracted from @lumenflow/initiatives to break circular dependency.
6
+ *
7
+ * @module @lumenflow/core/cycle-detector
8
+ */
9
+ /**
10
+ * Detects circular dependencies in WU dependency graph using DFS
11
+ *
12
+ * Uses standard cycle detection: tracks visited nodes and nodes in current
13
+ * recursion stack. If we encounter a node already in the recursion stack,
14
+ * we've found a cycle.
15
+ *
16
+ * Note: This function treats both `blocks` and `blocked_by` as edges for
17
+ * traversal. This means if WU-A blocks WU-B, and WU-B's blocked_by includes
18
+ * WU-A, following both directions will find a path back to WU-A.
19
+ *
20
+ * @param wuMap - Map of WU ID to WU object
21
+ * @returns Cycle detection result with hasCycle boolean and cycles array
22
+ *
23
+ * @example
24
+ * const wuMap = new Map([
25
+ * ['WU-001', { id: 'WU-001', blocks: ['WU-002'] }],
26
+ * ['WU-002', { id: 'WU-002', blocks: ['WU-001'] }],
27
+ * ]);
28
+ * const result = detectCycles(wuMap);
29
+ * // result.hasCycle === true
30
+ * // result.cycles contains the cycle path
31
+ */
32
+ export function detectCycles(wuMap) {
33
+ const visited = new Set();
34
+ const recursionStack = new Set();
35
+ const cycles = [];
36
+ /**
37
+ * DFS traversal to detect cycles
38
+ * @param wuId - Current WU ID
39
+ * @param path - Current path from root
40
+ * @returns True if cycle found
41
+ */
42
+ function dfs(wuId, path) {
43
+ // If node is in recursion stack, we found a cycle
44
+ if (recursionStack.has(wuId)) {
45
+ const cycleStart = path.indexOf(wuId);
46
+ if (cycleStart !== -1) {
47
+ cycles.push([...path.slice(cycleStart), wuId]);
48
+ }
49
+ else {
50
+ // Self-reference case
51
+ cycles.push([wuId, wuId]);
52
+ }
53
+ return true;
54
+ }
55
+ // If already fully visited, skip
56
+ if (visited.has(wuId)) {
57
+ return false;
58
+ }
59
+ // Mark as being processed
60
+ visited.add(wuId);
61
+ recursionStack.add(wuId);
62
+ // Get dependencies (both blocks and blocked_by create edges)
63
+ // Only use arrays - ignore legacy string format for backward compatibility
64
+ const wu = wuMap.get(wuId);
65
+ const blocks = Array.isArray(wu?.blocks) ? wu.blocks : [];
66
+ const blockedBy = Array.isArray(wu?.blocked_by) ? wu.blocked_by : [];
67
+ const deps = [...blocks, ...blockedBy];
68
+ // Visit all dependencies
69
+ for (const dep of deps) {
70
+ // Only traverse if the dependency exists in our map
71
+ if (wuMap.has(dep)) {
72
+ dfs(dep, [...path, wuId]);
73
+ }
74
+ }
75
+ // Remove from recursion stack (done processing)
76
+ recursionStack.delete(wuId);
77
+ return false;
78
+ }
79
+ // Run DFS from each node to find all cycles
80
+ for (const wuId of wuMap.keys()) {
81
+ if (!visited.has(wuId)) {
82
+ dfs(wuId, []);
83
+ }
84
+ }
85
+ return {
86
+ hasCycle: cycles.length > 0,
87
+ cycles,
88
+ };
89
+ }
@@ -3,17 +3,7 @@ import path from 'node:path';
3
3
  import { readWU } from './wu-yaml.js';
4
4
  import { WU_PATHS } from './wu-paths.js';
5
5
  import { STRING_LITERALS, WU_STATUS } from './wu-constants.js';
6
- // Optional import from @lumenflow/initiatives - if not available, provide stub
7
- let detectCycles;
8
- try {
9
- // Dynamic import for optional peer dependency
10
- const module = await import('@lumenflow/initiatives');
11
- detectCycles = module.detectCycles;
12
- }
13
- catch {
14
- // Fallback stub if @lumenflow/initiatives is not available
15
- detectCycles = () => ({ hasCycle: false, cycles: [] });
16
- }
6
+ import { detectCycles } from './cycle-detector.js';
17
7
  /**
18
8
  * Dependency Graph Module (WU-1247, WU-1568)
19
9
  *
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './date-utils.js';
8
8
  export * from './error-handler.js';
9
9
  export * from './retry-strategy.js';
10
10
  export * from './beacon-migration.js';
11
+ export * from './cycle-detector.js';
11
12
  export { DEFAULT_DOMAIN, inferDefaultDomain, normalizeToEmail, isValidEmail, } from './user-normalizer.js';
12
13
  export * from './git-adapter.js';
13
14
  export * from './state-machine.js';
@@ -47,3 +48,4 @@ export * from './agent-patterns-registry.js';
47
48
  export * from './lumenflow-home.js';
48
49
  export * from './force-bypass-audit.js';
49
50
  export { LUMENFLOW_PATHS, BEACON_PATHS } from './wu-constants.js';
51
+ export * from './color-support.js';
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ export * from './error-handler.js';
11
11
  export * from './retry-strategy.js';
12
12
  // Migration utilities (WU-1075)
13
13
  export * from './beacon-migration.js';
14
+ // Cycle detection (WU-1088 - extracted from initiatives to break circular dependency)
15
+ export * from './cycle-detector.js';
14
16
  // User normalizer (explicit exports to avoid conflicts)
15
17
  export { DEFAULT_DOMAIN, inferDefaultDomain, normalizeToEmail, isValidEmail, } from './user-normalizer.js';
16
18
  // Git operations
@@ -73,3 +75,5 @@ export * from './lumenflow-home.js';
73
75
  export * from './force-bypass-audit.js';
74
76
  // WU-1075: LumenFlow directory paths (exported from wu-constants)
75
77
  export { LUMENFLOW_PATHS, BEACON_PATHS } from './wu-constants.js';
78
+ // WU-1085: Color support for NO_COLOR/FORCE_COLOR/--no-color
79
+ export * from './color-support.js';
@@ -53,6 +53,8 @@ export declare const GitConfigSchema: z.ZodObject<{
53
53
  branchDriftWarning: z.ZodDefault<z.ZodNumber>;
54
54
  branchDriftInfo: z.ZodDefault<z.ZodNumber>;
55
55
  agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
56
+ agentBranchPatternsOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
57
+ disableAgentPatternRegistry: z.ZodDefault<z.ZodBoolean>;
56
58
  }, z.core.$strip>;
57
59
  /**
58
60
  * WU (Work Unit) configuration
@@ -245,6 +247,8 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
245
247
  branchDriftWarning: z.ZodDefault<z.ZodNumber>;
246
248
  branchDriftInfo: z.ZodDefault<z.ZodNumber>;
247
249
  agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
250
+ agentBranchPatternsOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
251
+ disableAgentPatternRegistry: z.ZodDefault<z.ZodBoolean>;
248
252
  }, z.core.$strip>>;
249
253
  wu: z.ZodDefault<z.ZodObject<{
250
254
  idPattern: z.ZodDefault<z.ZodString>;
@@ -394,6 +398,8 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
394
398
  branchDriftWarning: number;
395
399
  branchDriftInfo: number;
396
400
  agentBranchPatterns: string[];
401
+ disableAgentPatternRegistry: boolean;
402
+ agentBranchPatternsOverride?: string[];
397
403
  };
398
404
  wu: {
399
405
  idPattern: string;
@@ -86,12 +86,55 @@ export const GitConfigSchema = z.object({
86
86
  /** Info threshold for branch drift */
87
87
  branchDriftInfo: z.number().int().positive().default(10),
88
88
  /**
89
- * Agent branch patterns that bypass worktree requirements.
90
- * Branches matching these glob patterns can work in the main checkout.
91
- * Default: ['agent/*'] - narrow default, add vendor patterns as needed.
89
+ * Agent branch patterns to MERGE with the registry patterns.
90
+ * These patterns are merged with patterns from lumenflow.dev/registry/agent-patterns.json.
91
+ * Use this to add custom patterns that should work alongside the standard vendor patterns.
92
92
  * Protected branches (mainBranch + 'master') are NEVER bypassed.
93
+ *
94
+ * WU-1089: Changed default from ['agent/*'] to [] to allow registry to be used by default.
95
+ *
96
+ * @example
97
+ * ```yaml
98
+ * git:
99
+ * agentBranchPatterns:
100
+ * - 'my-custom-agent/*'
101
+ * - 'internal-tool/*'
102
+ * ```
93
103
  */
94
- agentBranchPatterns: z.array(z.string()).default(['agent/*']),
104
+ agentBranchPatterns: z.array(z.string()).default([]),
105
+ /**
106
+ * Agent branch patterns that REPLACE the registry patterns entirely.
107
+ * When set, these patterns are used instead of fetching from the registry.
108
+ * The agentBranchPatterns field is ignored when this is set.
109
+ *
110
+ * Use this for strict control over which agent patterns are allowed.
111
+ *
112
+ * @example
113
+ * ```yaml
114
+ * git:
115
+ * agentBranchPatternsOverride:
116
+ * - 'claude/*'
117
+ * - 'codex/*'
118
+ * ```
119
+ */
120
+ agentBranchPatternsOverride: z.array(z.string()).optional(),
121
+ /**
122
+ * Disable fetching agent patterns from the registry (airgapped mode).
123
+ * When true, only uses agentBranchPatterns from config or defaults to ['agent/*'].
124
+ * Useful for environments without network access or strict security requirements.
125
+ *
126
+ * @default false
127
+ *
128
+ * @example
129
+ * ```yaml
130
+ * git:
131
+ * disableAgentPatternRegistry: true
132
+ * agentBranchPatterns:
133
+ * - 'claude/*'
134
+ * - 'cursor/*'
135
+ * ```
136
+ */
137
+ disableAgentPatternRegistry: z.boolean().default(false),
95
138
  });
96
139
  /**
97
140
  * WU (Work Unit) configuration
@@ -2,6 +2,7 @@
2
2
  * Preflight validation helpers for wu:done.
3
3
  */
4
4
  import { execSync as execSyncImport } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
5
6
  import { validatePreflight } from './wu-preflight-validators.js';
6
7
  import { LOG_PREFIX, EMOJI, STDIO } from './wu-constants.js';
7
8
  /**
@@ -164,8 +165,14 @@ export function validateAllPreCommitHooks(id, worktreePath = null, options = {})
164
165
  if (worktreePath) {
165
166
  execOptions.cwd = worktreePath;
166
167
  }
168
+ // WU-1086: Check for .mjs extension first, fall back to .js for backwards compatibility
169
+ const basePath = worktreePath || '.';
170
+ const mjsPath = `${basePath}/tools/gates-pre-commit.mjs`;
171
+ const gateScript = existsSync(mjsPath)
172
+ ? 'tools/gates-pre-commit.mjs'
173
+ : 'tools/gates-pre-commit.js';
167
174
  // Run the gates-pre-commit script that contains all validation gates
168
- execSyncFn('node tools/gates-pre-commit.js', execOptions);
175
+ execSyncFn(`node ${gateScript}`, execOptions);
169
176
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All pre-commit hooks passed`);
170
177
  return { valid: true, errors: [] };
171
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/core",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Core WU lifecycle tools for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -66,6 +66,7 @@
66
66
  "README.md"
67
67
  ],
68
68
  "dependencies": {
69
+ "chalk": "^5.6.2",
69
70
  "change-case": "^5.4.4",
70
71
  "cli-progress": "^3.12.0",
71
72
  "cli-table3": "^0.6.5",
@@ -91,15 +92,11 @@
91
92
  "vitest": "^4.0.17"
92
93
  },
93
94
  "peerDependencies": {
94
- "@lumenflow/memory": "1.4.0",
95
- "@lumenflow/initiatives": "1.4.0"
95
+ "@lumenflow/memory": "1.5.0"
96
96
  },
97
97
  "peerDependenciesMeta": {
98
98
  "@lumenflow/memory": {
99
99
  "optional": true
100
- },
101
- "@lumenflow/initiatives": {
102
- "optional": true
103
100
  }
104
101
  },
105
102
  "engines": {