@lumenflow/core 1.3.6 → 1.4.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
@@ -68,6 +68,29 @@ const exists = await worktrees.exists('/path/to/worktree');
68
68
  await worktrees.remove('worktrees/operations-wu-123');
69
69
  ```
70
70
 
71
+ ### Agent Branch Patterns
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.
74
+
75
+ ```typescript
76
+ import { isAgentBranch, getAgentPatterns } from '@lumenflow/core';
77
+
78
+ // Check if branch can bypass worktree requirements (async, uses registry)
79
+ if (await isAgentBranch('claude/session-12345')) {
80
+ console.log('Agent branch - bypass allowed');
81
+ }
82
+
83
+ // Get the current list of agent patterns
84
+ const patterns = await getAgentPatterns();
85
+ // ['agent/*', 'claude/*', 'codex/*', 'copilot/*', 'cursor/*', ...]
86
+
87
+ // Synchronous version (uses local config only, no registry fetch)
88
+ import { isAgentBranchSync } from '@lumenflow/core';
89
+ const result = isAgentBranchSync('agent/task-123');
90
+ ```
91
+
92
+ Protected branches (main, master, lane/\*) are **never** bypassed, regardless of patterns.
93
+
71
94
  ## API Reference
72
95
 
73
96
  ### GitAdapter
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Agent Patterns Registry
3
+ *
4
+ * Central registry for AI agent branch patterns (claude/*, codex/*, copilot/*, cursor/*, etc.)
5
+ * that bypass worktree requirements. Static JSON served from lumenflow.dev,
6
+ * cached locally for 7 days with fallback to defaults.
7
+ *
8
+ * @module agent-patterns-registry
9
+ */
10
+ /** Default agent branch patterns (narrow: just agent/*) */
11
+ export declare const DEFAULT_AGENT_PATTERNS: string[];
12
+ /** Remote registry URL */
13
+ export declare const REGISTRY_URL = "https://lumenflow.dev/registry/agent-patterns.json";
14
+ /** Cache TTL: 7 days in milliseconds */
15
+ export declare const CACHE_TTL_MS: number;
16
+ /**
17
+ * Options for getAgentPatterns
18
+ */
19
+ interface GetAgentPatternsOptions {
20
+ /** Override cache directory (default: ~/.lumenflow/cache) */
21
+ cacheDir?: string;
22
+ /** Fetch timeout in milliseconds (default: 5000) */
23
+ timeoutMs?: number;
24
+ }
25
+ /**
26
+ * Get the default cache directory
27
+ *
28
+ * @returns Path to cache directory (~/.lumenflow/cache or LUMENFLOW_HOME/cache)
29
+ */
30
+ export declare function getCacheDir(): string;
31
+ /**
32
+ * Get agent branch patterns from registry with caching
33
+ *
34
+ * Fetches patterns from remote registry, caches locally for 7 days,
35
+ * and falls back to defaults on failure.
36
+ *
37
+ * @param options - Options
38
+ * @returns Array of agent branch patterns (glob format)
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const patterns = await getAgentPatterns();
43
+ * // ['claude/*', 'codex/*', 'copilot/*', 'cursor/*', 'agent/*']
44
+ * ```
45
+ */
46
+ export declare function getAgentPatterns(options?: GetAgentPatternsOptions): Promise<string[]>;
47
+ /**
48
+ * Clear the in-memory cache
49
+ *
50
+ * Used primarily for testing.
51
+ */
52
+ export declare function clearCache(): void;
53
+ export {};
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Agent Patterns Registry
3
+ *
4
+ * Central registry for AI agent branch patterns (claude/*, codex/*, copilot/*, cursor/*, etc.)
5
+ * that bypass worktree requirements. Static JSON served from lumenflow.dev,
6
+ * cached locally for 7 days with fallback to defaults.
7
+ *
8
+ * @module agent-patterns-registry
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as os from 'node:os';
13
+ /** Default agent branch patterns (narrow: just agent/*) */
14
+ export const DEFAULT_AGENT_PATTERNS = ['agent/*'];
15
+ /** Remote registry URL */
16
+ export const REGISTRY_URL = 'https://lumenflow.dev/registry/agent-patterns.json';
17
+ /** Cache TTL: 7 days in milliseconds */
18
+ export const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
19
+ /** Cache file name */
20
+ const CACHE_FILE_NAME = 'agent-patterns-cache.json';
21
+ /** Default fetch timeout in milliseconds */
22
+ const DEFAULT_TIMEOUT_MS = 5000;
23
+ /** In-memory cache for patterns */
24
+ let memoryCache = null;
25
+ /** In-memory cache timestamp */
26
+ let memoryCacheTime = 0;
27
+ /**
28
+ * Get the default cache directory
29
+ *
30
+ * @returns Path to cache directory (~/.lumenflow/cache or LUMENFLOW_HOME/cache)
31
+ */
32
+ export function getCacheDir() {
33
+ const lumenflowHome = process.env.LUMENFLOW_HOME;
34
+ if (lumenflowHome) {
35
+ return path.join(lumenflowHome, 'cache');
36
+ }
37
+ return path.join(os.homedir(), '.lumenflow', 'cache');
38
+ }
39
+ /**
40
+ * Validate registry response
41
+ *
42
+ * @param data - Data to validate
43
+ * @returns True if valid registry response
44
+ */
45
+ function isValidRegistryResponse(data) {
46
+ if (!data || typeof data !== 'object')
47
+ return false;
48
+ const obj = data;
49
+ if (!Array.isArray(obj.patterns))
50
+ return false;
51
+ if (!obj.patterns.every((p) => typeof p === 'string'))
52
+ return false;
53
+ return true;
54
+ }
55
+ /**
56
+ * Read cache from disk
57
+ *
58
+ * @param cacheDir - Cache directory
59
+ * @returns Cache data or null if not found/invalid
60
+ */
61
+ function readCache(cacheDir) {
62
+ try {
63
+ const cacheFile = path.join(cacheDir, CACHE_FILE_NAME);
64
+ if (!fs.existsSync(cacheFile))
65
+ return null;
66
+ const content = fs.readFileSync(cacheFile, 'utf8');
67
+ const data = JSON.parse(content);
68
+ // Validate cache structure
69
+ if (!Array.isArray(data.patterns))
70
+ return null;
71
+ if (typeof data.fetchedAt !== 'number')
72
+ return null;
73
+ return data;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Write cache to disk
81
+ *
82
+ * @param cacheDir - Cache directory
83
+ * @param data - Data to cache
84
+ */
85
+ function writeCache(cacheDir, data) {
86
+ try {
87
+ // Ensure cache directory exists
88
+ if (!fs.existsSync(cacheDir)) {
89
+ fs.mkdirSync(cacheDir, { recursive: true });
90
+ }
91
+ const cacheFile = path.join(cacheDir, CACHE_FILE_NAME);
92
+ fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
93
+ }
94
+ catch {
95
+ // Fail silently - cache is optional
96
+ }
97
+ }
98
+ /**
99
+ * Fetch patterns from remote registry with timeout
100
+ *
101
+ * @param timeoutMs - Timeout in milliseconds
102
+ * @returns Registry response or null on failure
103
+ */
104
+ async function fetchFromRegistry(timeoutMs) {
105
+ try {
106
+ const controller = new AbortController();
107
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
108
+ const response = await fetch(REGISTRY_URL, {
109
+ signal: controller.signal,
110
+ headers: {
111
+ Accept: 'application/json',
112
+ 'User-Agent': 'lumenflow-core',
113
+ },
114
+ });
115
+ clearTimeout(timeoutId);
116
+ if (!response.ok) {
117
+ return null;
118
+ }
119
+ const data = await response.json();
120
+ if (!isValidRegistryResponse(data)) {
121
+ return null;
122
+ }
123
+ return data;
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Get agent branch patterns from registry with caching
131
+ *
132
+ * Fetches patterns from remote registry, caches locally for 7 days,
133
+ * and falls back to defaults on failure.
134
+ *
135
+ * @param options - Options
136
+ * @returns Array of agent branch patterns (glob format)
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * const patterns = await getAgentPatterns();
141
+ * // ['claude/*', 'codex/*', 'copilot/*', 'cursor/*', 'agent/*']
142
+ * ```
143
+ */
144
+ export async function getAgentPatterns(options = {}) {
145
+ const { cacheDir = getCacheDir(), timeoutMs = DEFAULT_TIMEOUT_MS } = options;
146
+ // Check memory cache first (fast path)
147
+ const now = Date.now();
148
+ if (memoryCache && now - memoryCacheTime < CACHE_TTL_MS) {
149
+ return memoryCache;
150
+ }
151
+ // Check disk cache
152
+ const diskCache = readCache(cacheDir);
153
+ if (diskCache && now - diskCache.fetchedAt < CACHE_TTL_MS) {
154
+ // Fresh disk cache - use it and update memory cache
155
+ memoryCache = diskCache.patterns;
156
+ memoryCacheTime = diskCache.fetchedAt;
157
+ return diskCache.patterns;
158
+ }
159
+ // Cache is stale or missing - try to fetch
160
+ const registryData = await fetchFromRegistry(timeoutMs);
161
+ if (registryData) {
162
+ // Update caches
163
+ const cacheData = {
164
+ version: registryData.version,
165
+ patterns: registryData.patterns,
166
+ fetchedAt: now,
167
+ };
168
+ writeCache(cacheDir, cacheData);
169
+ memoryCache = registryData.patterns;
170
+ memoryCacheTime = now;
171
+ return registryData.patterns;
172
+ }
173
+ // Fetch failed - try stale cache as fallback
174
+ if (diskCache) {
175
+ memoryCache = diskCache.patterns;
176
+ memoryCacheTime = diskCache.fetchedAt; // Keep stale time
177
+ return diskCache.patterns;
178
+ }
179
+ // No cache, no network - use defaults
180
+ return DEFAULT_AGENT_PATTERNS;
181
+ }
182
+ /**
183
+ * Clear the in-memory cache
184
+ *
185
+ * Used primarily for testing.
186
+ */
187
+ export function clearCache() {
188
+ memoryCache = null;
189
+ memoryCacheTime = 0;
190
+ }
@@ -8,12 +8,34 @@
8
8
  */
9
9
  /**
10
10
  * Check if branch is an agent branch that can bypass worktree requirements.
11
- * Uses the existing config loader (which handles caching/validation).
11
+ *
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
+ *
16
+ * @param branch - Branch name to check
17
+ * @returns Promise<true> if branch matches agent patterns
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * if (await isAgentBranch('claude/session-123')) {
22
+ * // Allow bypass for agent branch
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function isAgentBranch(branch: string | null | undefined): Promise<boolean>;
27
+ /**
28
+ * Synchronous version of isAgentBranch for backwards compatibility.
29
+ *
30
+ * Uses only local config patterns or defaults - does NOT fetch from registry.
31
+ * Prefer async isAgentBranch() when possible.
12
32
  *
13
33
  * @param branch - Branch name to check
14
34
  * @returns True if branch matches agent patterns
35
+ *
36
+ * @deprecated Use async isAgentBranch() instead for registry support
15
37
  */
16
- export declare function isAgentBranch(branch: string | null | undefined): boolean;
38
+ export declare function isAgentBranchSync(branch: string | null | undefined): boolean;
17
39
  /**
18
40
  * Check if headless mode is allowed (guarded).
19
41
  * Requires LUMENFLOW_HEADLESS=1 AND (LUMENFLOW_ADMIN=1 OR CI truthy OR GITHUB_ACTIONS truthy)
@@ -8,8 +8,7 @@
8
8
  */
9
9
  import micromatch from 'micromatch';
10
10
  import { getConfig } from './lumenflow-config.js';
11
- /** Default agent branch patterns (narrow: just agent/*) */
12
- const DEFAULT_AGENT_BRANCH_PATTERNS = ['agent/*'];
11
+ import { getAgentPatterns, DEFAULT_AGENT_PATTERNS } from './agent-patterns-registry.js';
13
12
  /** Legacy protected branch (always protected regardless of mainBranch setting) */
14
13
  const LEGACY_PROTECTED = 'master';
15
14
  /**
@@ -36,12 +35,62 @@ function getProtectedBranches() {
36
35
  }
37
36
  /**
38
37
  * Check if branch is an agent branch that can bypass worktree requirements.
39
- * Uses the existing config loader (which handles caching/validation).
38
+ *
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.
42
+ *
43
+ * @param branch - Branch name to check
44
+ * @returns Promise<true> if branch matches agent patterns
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * if (await isAgentBranch('claude/session-123')) {
49
+ * // Allow bypass for agent branch
50
+ * }
51
+ * ```
52
+ */
53
+ export async function isAgentBranch(branch) {
54
+ // Fail-closed: no branch = protected
55
+ if (!branch)
56
+ return false;
57
+ // Detached HEAD = protected (fail-closed)
58
+ if (branch === 'HEAD')
59
+ return false;
60
+ // Load config (uses existing loader with caching)
61
+ const config = getConfig();
62
+ const protectedBranches = getProtectedBranches();
63
+ // Protected branches are NEVER bypassed (mainBranch + 'master')
64
+ if (protectedBranches.includes(branch))
65
+ return false;
66
+ // LumenFlow lane branches require worktrees (uses config's laneBranchPrefix)
67
+ if (getLaneBranchPattern().test(branch))
68
+ 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;
74
+ }
75
+ else {
76
+ // Fetch from registry (with caching and fallback to defaults)
77
+ patterns = await getAgentPatterns();
78
+ }
79
+ // Use micromatch for proper glob matching
80
+ return micromatch.isMatch(branch, patterns);
81
+ }
82
+ /**
83
+ * Synchronous version of isAgentBranch for backwards compatibility.
84
+ *
85
+ * Uses only local config patterns or defaults - does NOT fetch from registry.
86
+ * Prefer async isAgentBranch() when possible.
40
87
  *
41
88
  * @param branch - Branch name to check
42
89
  * @returns True if branch matches agent patterns
90
+ *
91
+ * @deprecated Use async isAgentBranch() instead for registry support
43
92
  */
44
- export function isAgentBranch(branch) {
93
+ export function isAgentBranchSync(branch) {
45
94
  // Fail-closed: no branch = protected
46
95
  if (!branch)
47
96
  return false;
@@ -51,15 +100,16 @@ export function isAgentBranch(branch) {
51
100
  // Load config (uses existing loader with caching)
52
101
  const config = getConfig();
53
102
  const protectedBranches = getProtectedBranches();
54
- const patterns = config?.git?.agentBranchPatterns?.length > 0
55
- ? config.git.agentBranchPatterns
56
- : DEFAULT_AGENT_BRANCH_PATTERNS;
57
103
  // Protected branches are NEVER bypassed (mainBranch + 'master')
58
104
  if (protectedBranches.includes(branch))
59
105
  return false;
60
106
  // LumenFlow lane branches require worktrees (uses config's laneBranchPrefix)
61
107
  if (getLaneBranchPattern().test(branch))
62
108
  return false;
109
+ // Use config patterns or defaults (no registry fetch in sync version)
110
+ const patterns = config?.git?.agentBranchPatterns?.length > 0
111
+ ? config.git.agentBranchPatterns
112
+ : DEFAULT_AGENT_PATTERNS;
63
113
  // Use micromatch for proper glob matching
64
114
  return micromatch.isMatch(branch, patterns);
65
115
  }
@@ -3,6 +3,8 @@
3
3
  * CLI helper for bash hooks to check if branch is an agent branch.
4
4
  * Uses the same isAgentBranch() logic as TypeScript code.
5
5
  *
6
+ * Now async to support registry pattern lookup with fetch + cache.
7
+ *
6
8
  * Usage: node dist/cli/is-agent-branch.js [branch-name]
7
9
  * Exit codes: 0 = agent branch (allowed), 1 = not agent branch (protected)
8
10
  *
@@ -3,13 +3,22 @@
3
3
  * CLI helper for bash hooks to check if branch is an agent branch.
4
4
  * Uses the same isAgentBranch() logic as TypeScript code.
5
5
  *
6
+ * Now async to support registry pattern lookup with fetch + cache.
7
+ *
6
8
  * Usage: node dist/cli/is-agent-branch.js [branch-name]
7
9
  * Exit codes: 0 = agent branch (allowed), 1 = not agent branch (protected)
8
10
  *
9
11
  * @module cli/is-agent-branch
10
12
  */
11
13
  import { isAgentBranch } from '../branch-check.js';
12
- const branch = process.argv[2] || null;
13
- const result = isAgentBranch(branch);
14
- // Exit 0 = agent branch (truthy), Exit 1 = not agent branch
15
- process.exit(result ? 0 : 1);
14
+ async function main() {
15
+ const branch = process.argv[2] || null;
16
+ const result = await isAgentBranch(branch);
17
+ // Exit 0 = agent branch (truthy), Exit 1 = not agent branch
18
+ process.exit(result ? 0 : 1);
19
+ }
20
+ main().catch((error) => {
21
+ console.error('Error checking agent branch:', error.message);
22
+ // Fail-closed: error = not allowed
23
+ process.exit(1);
24
+ });
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export * from './lumenflow-config.js';
43
43
  export * from './lumenflow-config-schema.js';
44
44
  export * from './gates-config.js';
45
45
  export * from './branch-check.js';
46
+ export * from './agent-patterns-registry.js';
46
47
  export * from './lumenflow-home.js';
47
48
  export * from './force-bypass-audit.js';
48
49
  export { LUMENFLOW_PATHS, BEACON_PATHS } from './wu-constants.js';
package/dist/index.js CHANGED
@@ -65,6 +65,8 @@ export * from './lumenflow-config-schema.js';
65
65
  export * from './gates-config.js';
66
66
  // Branch check utilities
67
67
  export * from './branch-check.js';
68
+ // WU-1082: Agent patterns registry (fetch + cache)
69
+ export * from './agent-patterns-registry.js';
68
70
  // WU-1062: External plan storage
69
71
  export * from './lumenflow-home.js';
70
72
  // WU-1070: Force bypass audit logging
@@ -27,6 +27,7 @@
27
27
  * @see {@link tools/wu-edit.mjs} - Spec edits (WU-1274)
28
28
  * @see {@link tools/initiative-create.mjs} - Initiative creation (WU-1439)
29
29
  */
30
+ import type { GitAdapter } from './git-adapter.js';
30
31
  /**
31
32
  * Maximum retry attempts for ff-only merge when main moves
32
33
  *
@@ -34,6 +35,18 @@
34
35
  * concurrently. Each retry fetches latest main and rebases.
35
36
  */
36
37
  export declare const MAX_MERGE_RETRIES = 3;
38
+ /**
39
+ * Environment variable name for LUMENFLOW_FORCE bypass
40
+ *
41
+ * WU-1081: Exported for use in micro-worktree push operations.
42
+ */
43
+ export declare const LUMENFLOW_FORCE_ENV = "LUMENFLOW_FORCE";
44
+ /**
45
+ * Environment variable name for LUMENFLOW_FORCE_REASON audit trail
46
+ *
47
+ * WU-1081: Exported for use in micro-worktree push operations.
48
+ */
49
+ export declare const LUMENFLOW_FORCE_REASON_ENV = "LUMENFLOW_FORCE_REASON";
37
50
  /**
38
51
  * Default log prefix for micro-worktree operations
39
52
  *
@@ -135,6 +148,25 @@ export declare function formatFiles(files: any, worktreePath: any, logPrefix?: s
135
148
  * @throws {Error} If merge fails after all retries
136
149
  */
137
150
  export declare function mergeWithRetry(tempBranchName: any, microWorktreePath: any, logPrefix?: string): Promise<void>;
151
+ /**
152
+ * Push using refspec with LUMENFLOW_FORCE to bypass pre-push hooks
153
+ *
154
+ * WU-1081: Micro-worktree pushes to origin/main need to bypass pre-push hooks
155
+ * because they operate from temp branches in /tmp directories, which would
156
+ * otherwise be blocked by hook validation.
157
+ *
158
+ * Sets LUMENFLOW_FORCE=1 and LUMENFLOW_FORCE_REASON during the push,
159
+ * then restores original environment values (even on error).
160
+ *
161
+ * @param {GitAdapter} gitAdapter - GitAdapter instance to use for push
162
+ * @param {string} remote - Remote name (e.g., 'origin')
163
+ * @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
164
+ * @param {string} remoteRef - Remote ref to update (e.g., 'main')
165
+ * @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
166
+ * @returns {Promise<void>}
167
+ * @throws {Error} If push fails (env vars still restored)
168
+ */
169
+ export declare function pushRefspecWithForce(gitAdapter: GitAdapter, remote: string, localRef: string, remoteRef: string, reason: string): Promise<void>;
138
170
  /**
139
171
  * Execute an operation in a micro-worktree with full isolation
140
172
  *
@@ -40,6 +40,18 @@ import { BRANCHES, REMOTES, GIT_REFS, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, STDI
40
40
  * concurrently. Each retry fetches latest main and rebases.
41
41
  */
42
42
  export const MAX_MERGE_RETRIES = 3;
43
+ /**
44
+ * Environment variable name for LUMENFLOW_FORCE bypass
45
+ *
46
+ * WU-1081: Exported for use in micro-worktree push operations.
47
+ */
48
+ export const LUMENFLOW_FORCE_ENV = 'LUMENFLOW_FORCE';
49
+ /**
50
+ * Environment variable name for LUMENFLOW_FORCE_REASON audit trail
51
+ *
52
+ * WU-1081: Exported for use in micro-worktree push operations.
53
+ */
54
+ export const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
43
55
  /**
44
56
  * Default log prefix for micro-worktree operations
45
57
  *
@@ -344,6 +356,51 @@ export async function mergeWithRetry(tempBranchName, microWorktreePath, logPrefi
344
356
  }
345
357
  }
346
358
  }
359
+ /**
360
+ * Push using refspec with LUMENFLOW_FORCE to bypass pre-push hooks
361
+ *
362
+ * WU-1081: Micro-worktree pushes to origin/main need to bypass pre-push hooks
363
+ * because they operate from temp branches in /tmp directories, which would
364
+ * otherwise be blocked by hook validation.
365
+ *
366
+ * Sets LUMENFLOW_FORCE=1 and LUMENFLOW_FORCE_REASON during the push,
367
+ * then restores original environment values (even on error).
368
+ *
369
+ * @param {GitAdapter} gitAdapter - GitAdapter instance to use for push
370
+ * @param {string} remote - Remote name (e.g., 'origin')
371
+ * @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
372
+ * @param {string} remoteRef - Remote ref to update (e.g., 'main')
373
+ * @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
374
+ * @returns {Promise<void>}
375
+ * @throws {Error} If push fails (env vars still restored)
376
+ */
377
+ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteRef, reason) {
378
+ // Save original env values
379
+ const originalForce = process.env[LUMENFLOW_FORCE_ENV];
380
+ const originalReason = process.env[LUMENFLOW_FORCE_REASON_ENV];
381
+ try {
382
+ // Set LUMENFLOW_FORCE for the push
383
+ process.env[LUMENFLOW_FORCE_ENV] = '1';
384
+ process.env[LUMENFLOW_FORCE_REASON_ENV] = reason;
385
+ // Perform the push
386
+ await gitAdapter.pushRefspec(remote, localRef, remoteRef);
387
+ }
388
+ finally {
389
+ // Restore original env values
390
+ if (originalForce === undefined) {
391
+ delete process.env[LUMENFLOW_FORCE_ENV];
392
+ }
393
+ else {
394
+ process.env[LUMENFLOW_FORCE_ENV] = originalForce;
395
+ }
396
+ if (originalReason === undefined) {
397
+ delete process.env[LUMENFLOW_FORCE_REASON_ENV];
398
+ }
399
+ else {
400
+ process.env[LUMENFLOW_FORCE_REASON_ENV] = originalReason;
401
+ }
402
+ }
403
+ }
347
404
  /**
348
405
  * Execute an operation in a micro-worktree with full isolation
349
406
  *
@@ -401,8 +458,9 @@ export async function withMicroWorktree(options) {
401
458
  // Step 6: Push to origin (different paths for pushOnly vs standard)
402
459
  if (pushOnly) {
403
460
  // WU-1435: Push directly to origin/main without touching local main
461
+ // WU-1081: Use LUMENFLOW_FORCE to bypass pre-push hooks for micro-worktree pushes
404
462
  console.log(`${logPrefix} Pushing directly to ${REMOTES.ORIGIN}/${BRANCHES.MAIN} (push-only)...`);
405
- await gitWorktree.pushRefspec(REMOTES.ORIGIN, tempBranchName, BRANCHES.MAIN);
463
+ await pushRefspecWithForce(gitWorktree, REMOTES.ORIGIN, tempBranchName, BRANCHES.MAIN, `micro-worktree push for ${operation} (automated)`);
406
464
  console.log(`${logPrefix} ✅ Pushed to ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
407
465
  // Fetch to update remote tracking ref (FETCH_HEAD)
408
466
  console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN}...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/core",
3
- "version": "1.3.6",
3
+ "version": "1.4.0",
4
4
  "description": "Core WU lifecycle tools for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -91,8 +91,8 @@
91
91
  "vitest": "^4.0.17"
92
92
  },
93
93
  "peerDependencies": {
94
- "@lumenflow/memory": "1.3.6",
95
- "@lumenflow/initiatives": "1.3.6"
94
+ "@lumenflow/memory": "1.4.0",
95
+ "@lumenflow/initiatives": "1.4.0"
96
96
  },
97
97
  "peerDependenciesMeta": {
98
98
  "@lumenflow/memory": {