@lumenflow/core 1.3.5 → 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 +23 -0
- package/dist/agent-patterns-registry.d.ts +53 -0
- package/dist/agent-patterns-registry.js +190 -0
- package/dist/branch-check.d.ts +24 -2
- package/dist/branch-check.js +57 -7
- package/dist/cli/is-agent-branch.d.ts +2 -0
- package/dist/cli/is-agent-branch.js +13 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/micro-worktree.d.ts +32 -0
- package/dist/micro-worktree.js +59 -1
- package/dist/state-machine.d.ts +1 -0
- package/dist/state-machine.js +2 -1
- package/dist/wu-consistency-checker.d.ts +22 -2
- package/dist/wu-consistency-checker.js +260 -30
- package/dist/wu-state-schema.d.ts +24 -1
- package/dist/wu-state-schema.js +13 -0
- package/dist/wu-state-store.d.ts +21 -0
- package/dist/wu-state-store.js +54 -0
- package/package.json +3 -3
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
|
+
}
|
package/dist/branch-check.d.ts
CHANGED
|
@@ -8,12 +8,34 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
10
|
* Check if branch is an agent branch that can bypass worktree requirements.
|
|
11
|
-
*
|
|
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
|
|
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)
|
package/dist/branch-check.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import micromatch from 'micromatch';
|
|
10
10
|
import { getConfig } from './lumenflow-config.js';
|
|
11
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
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
|
package/dist/micro-worktree.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/micro-worktree.js
CHANGED
|
@@ -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
|
|
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/dist/state-machine.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - in_progress → blocked (block)
|
|
10
10
|
* - in_progress → waiting (implementation complete, awaiting sign-off)
|
|
11
11
|
* - in_progress → done (direct completion)
|
|
12
|
+
* - in_progress → ready (release - WU-1080: orphan recovery)
|
|
12
13
|
* - blocked → in_progress (unblock)
|
|
13
14
|
* - blocked → done (blocker resolved, direct completion)
|
|
14
15
|
* - waiting → in_progress (changes requested)
|
package/dist/state-machine.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - in_progress → blocked (block)
|
|
10
10
|
* - in_progress → waiting (implementation complete, awaiting sign-off)
|
|
11
11
|
* - in_progress → done (direct completion)
|
|
12
|
+
* - in_progress → ready (release - WU-1080: orphan recovery)
|
|
12
13
|
* - blocked → in_progress (unblock)
|
|
13
14
|
* - blocked → done (blocker resolved, direct completion)
|
|
14
15
|
* - waiting → in_progress (changes requested)
|
|
@@ -26,7 +27,7 @@ const VALID_STATES = new Set(['ready', 'in_progress', 'blocked', 'waiting', 'don
|
|
|
26
27
|
*/
|
|
27
28
|
const TRANSITIONS = {
|
|
28
29
|
ready: ['in_progress'],
|
|
29
|
-
in_progress: ['blocked', 'waiting', 'done'],
|
|
30
|
+
in_progress: ['blocked', 'waiting', 'done', 'ready'], // WU-1080: 'ready' via release for orphan recovery
|
|
30
31
|
blocked: ['in_progress', 'done'],
|
|
31
32
|
waiting: ['in_progress', 'done'],
|
|
32
33
|
done: [], // Terminal state - no outgoing transitions
|
|
@@ -84,14 +84,34 @@ export interface RepairWUInconsistencyOptions {
|
|
|
84
84
|
projectRoot?: string;
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Error object structure from checkWUConsistency()
|
|
88
|
+
*/
|
|
89
|
+
interface ConsistencyError {
|
|
90
|
+
type: string;
|
|
91
|
+
wuId: string;
|
|
92
|
+
title?: string;
|
|
93
|
+
lane?: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
repairAction?: string;
|
|
96
|
+
canAutoRepair: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Repair WU inconsistencies using micro-worktree isolation (WU-1078)
|
|
100
|
+
*
|
|
101
|
+
* All file modifications (stamps, YAML, markdown) are made atomically
|
|
102
|
+
* in a micro-worktree, then committed and pushed to origin/main.
|
|
103
|
+
* This prevents direct writes to the main checkout.
|
|
88
104
|
*
|
|
89
105
|
* @param {object} report - Report from checkWUConsistency()
|
|
90
106
|
* @param {RepairWUInconsistencyOptions} [options={}] - Repair options
|
|
91
107
|
* @returns {Promise<object>} Result with repaired, skipped, and failed counts
|
|
92
108
|
*/
|
|
93
|
-
export declare function repairWUInconsistency(report:
|
|
109
|
+
export declare function repairWUInconsistency(report: {
|
|
110
|
+
valid: boolean;
|
|
111
|
+
errors: ConsistencyError[];
|
|
112
|
+
}, options?: RepairWUInconsistencyOptions): Promise<{
|
|
94
113
|
repaired: number;
|
|
95
114
|
skipped: number;
|
|
96
115
|
failed: number;
|
|
97
116
|
}>;
|
|
117
|
+
export {};
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
* @see {@link ../wu-repair.mjs} CLI interface
|
|
14
14
|
*/
|
|
15
15
|
import { readFile, writeFile, readdir, mkdir, access } from 'node:fs/promises';
|
|
16
|
-
import { constants } from 'node:fs';
|
|
16
|
+
import { constants, existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { parseYAML, stringifyYAML } from './wu-yaml.js';
|
|
19
19
|
import { WU_PATHS } from './wu-paths.js';
|
|
20
20
|
import { CONSISTENCY_TYPES, CONSISTENCY_MESSAGES, LOG_PREFIX, REMOTES, STRING_LITERALS, toKebab, WU_STATUS, YAML_OPTIONS, } from './wu-constants.js';
|
|
21
21
|
import { todayISO } from './date-utils.js';
|
|
22
22
|
import { createGitForPath } from './git-adapter.js';
|
|
23
|
+
import { withMicroWorktree } from './micro-worktree.js';
|
|
23
24
|
/**
|
|
24
25
|
* Check a single WU for state inconsistencies
|
|
25
26
|
*
|
|
@@ -238,7 +239,34 @@ export async function checkLaneForOrphanDoneWU(lane, excludeId, projectRoot = pr
|
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
/**
|
|
241
|
-
*
|
|
242
|
+
* Categorize errors into file-based repairs (need micro-worktree) and git-only repairs
|
|
243
|
+
*/
|
|
244
|
+
function categorizeErrors(errors) {
|
|
245
|
+
const fileRepairs = [];
|
|
246
|
+
const gitOnlyRepairs = [];
|
|
247
|
+
const nonRepairable = [];
|
|
248
|
+
for (const error of errors) {
|
|
249
|
+
if (!error.canAutoRepair) {
|
|
250
|
+
nonRepairable.push(error);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Git-only repairs: worktree/branch cleanup doesn't need micro-worktree
|
|
254
|
+
if (error.type === CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE) {
|
|
255
|
+
gitOnlyRepairs.push(error);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// All file-based repairs need micro-worktree isolation
|
|
259
|
+
fileRepairs.push(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { fileRepairs, gitOnlyRepairs, nonRepairable };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Repair WU inconsistencies using micro-worktree isolation (WU-1078)
|
|
266
|
+
*
|
|
267
|
+
* All file modifications (stamps, YAML, markdown) are made atomically
|
|
268
|
+
* in a micro-worktree, then committed and pushed to origin/main.
|
|
269
|
+
* This prevents direct writes to the main checkout.
|
|
242
270
|
*
|
|
243
271
|
* @param {object} report - Report from checkWUConsistency()
|
|
244
272
|
* @param {RepairWUInconsistencyOptions} [options={}] - Repair options
|
|
@@ -249,20 +277,72 @@ export async function repairWUInconsistency(report, options = {}) {
|
|
|
249
277
|
if (report.valid) {
|
|
250
278
|
return { repaired: 0, skipped: 0, failed: 0 };
|
|
251
279
|
}
|
|
280
|
+
const { fileRepairs, gitOnlyRepairs, nonRepairable } = categorizeErrors(report.errors);
|
|
252
281
|
let repaired = 0;
|
|
253
|
-
let skipped =
|
|
282
|
+
let skipped = nonRepairable.length;
|
|
254
283
|
let failed = 0;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
284
|
+
// Dry run mode: just count
|
|
285
|
+
if (dryRun) {
|
|
286
|
+
return {
|
|
287
|
+
repaired: fileRepairs.length + gitOnlyRepairs.length,
|
|
288
|
+
skipped,
|
|
289
|
+
failed: 0,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Step 1: Process file-based repairs via micro-worktree (batched)
|
|
293
|
+
if (fileRepairs.length > 0) {
|
|
294
|
+
try {
|
|
295
|
+
// Generate a batch ID from the WU IDs being repaired
|
|
296
|
+
const batchId = `batch-${fileRepairs.map((e) => e.wuId).join('-')}`.slice(0, 50);
|
|
297
|
+
await withMicroWorktree({
|
|
298
|
+
operation: 'wu-repair',
|
|
299
|
+
id: batchId,
|
|
300
|
+
logPrefix: LOG_PREFIX.REPAIR,
|
|
301
|
+
execute: async ({ worktreePath }) => {
|
|
302
|
+
const filesModified = [];
|
|
303
|
+
for (const error of fileRepairs) {
|
|
304
|
+
try {
|
|
305
|
+
const result = await repairSingleErrorInWorktree(error, worktreePath, projectRoot);
|
|
306
|
+
if (result.success && result.files) {
|
|
307
|
+
filesModified.push(...result.files);
|
|
308
|
+
repaired++;
|
|
309
|
+
}
|
|
310
|
+
else if (result.skipped) {
|
|
311
|
+
skipped++;
|
|
312
|
+
if (result.reason) {
|
|
313
|
+
console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
failed++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
322
|
+
console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
|
|
323
|
+
failed++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Deduplicate files
|
|
327
|
+
const uniqueFiles = [...new Set(filesModified)];
|
|
328
|
+
return {
|
|
329
|
+
commitMessage: `fix: repair ${repaired} WU inconsistencies`,
|
|
330
|
+
files: uniqueFiles,
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
});
|
|
259
334
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
335
|
+
catch (err) {
|
|
336
|
+
// If micro-worktree fails, mark all file repairs as failed
|
|
337
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
338
|
+
console.error(`${LOG_PREFIX.REPAIR} Micro-worktree operation failed: ${errMessage}`);
|
|
339
|
+
failed += fileRepairs.length - repaired;
|
|
263
340
|
}
|
|
341
|
+
}
|
|
342
|
+
// Step 2: Process git-only repairs (worktree/branch cleanup) directly
|
|
343
|
+
for (const error of gitOnlyRepairs) {
|
|
264
344
|
try {
|
|
265
|
-
const result = await
|
|
345
|
+
const result = await repairGitOnlyError(error, projectRoot);
|
|
266
346
|
if (result.success) {
|
|
267
347
|
repaired++;
|
|
268
348
|
}
|
|
@@ -285,34 +365,85 @@ export async function repairWUInconsistency(report, options = {}) {
|
|
|
285
365
|
return { repaired, skipped, failed };
|
|
286
366
|
}
|
|
287
367
|
/**
|
|
288
|
-
* Repair a single
|
|
368
|
+
* Repair a single file-based error inside a micro-worktree (WU-1078)
|
|
369
|
+
*
|
|
370
|
+
* This function performs file modifications inside the worktree path,
|
|
371
|
+
* which is then committed and pushed atomically by withMicroWorktree.
|
|
372
|
+
*
|
|
373
|
+
* @param {ConsistencyError} error - Error object from checkWUConsistency()
|
|
374
|
+
* @param {string} worktreePath - Path to the micro-worktree
|
|
375
|
+
* @param {string} projectRoot - Original project root (for reading source files)
|
|
376
|
+
* @returns {Promise<RepairResult>} Result with success, skipped, reason, and files modified
|
|
377
|
+
*/
|
|
378
|
+
async function repairSingleErrorInWorktree(error, worktreePath, projectRoot) {
|
|
379
|
+
switch (error.type) {
|
|
380
|
+
case CONSISTENCY_TYPES.YAML_DONE_NO_STAMP: {
|
|
381
|
+
const files = await createStampInWorktree(error.wuId, error.title || `WU ${error.wuId}`, worktreePath);
|
|
382
|
+
return { success: true, files };
|
|
383
|
+
}
|
|
384
|
+
case CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS: {
|
|
385
|
+
const files = await removeWUFromSectionInWorktree(WU_PATHS.STATUS(), error.wuId, '## In Progress', worktreePath, projectRoot);
|
|
386
|
+
return { success: true, files };
|
|
387
|
+
}
|
|
388
|
+
case CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION: {
|
|
389
|
+
const files = await removeWUFromSectionInWorktree(WU_PATHS.BACKLOG(), error.wuId, '## 🔧 In progress', worktreePath, projectRoot);
|
|
390
|
+
return { success: true, files };
|
|
391
|
+
}
|
|
392
|
+
case CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE: {
|
|
393
|
+
const files = await updateYamlToDoneInWorktree(error.wuId, worktreePath, projectRoot);
|
|
394
|
+
return { success: true, files };
|
|
395
|
+
}
|
|
396
|
+
default:
|
|
397
|
+
return { skipped: true, reason: `Unknown error type: ${error.type}` };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Repair git-only errors (worktree/branch cleanup) without micro-worktree
|
|
402
|
+
*
|
|
403
|
+
* These operations don't modify files in the repo, they only manage git worktrees
|
|
404
|
+
* and branches, so they can run directly.
|
|
289
405
|
*
|
|
290
|
-
* @param {
|
|
406
|
+
* @param {ConsistencyError} error - Error object
|
|
291
407
|
* @param {string} projectRoot - Project root directory
|
|
292
408
|
* @returns {Promise<RepairResult>} Result with success, skipped, and reason
|
|
293
409
|
*/
|
|
294
|
-
async function
|
|
410
|
+
async function repairGitOnlyError(error, projectRoot) {
|
|
295
411
|
switch (error.type) {
|
|
296
|
-
case CONSISTENCY_TYPES.YAML_DONE_NO_STAMP:
|
|
297
|
-
await createStampInProject(error.wuId, error.title || `WU ${error.wuId}`, projectRoot);
|
|
298
|
-
return { success: true };
|
|
299
|
-
case CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS:
|
|
300
|
-
await removeWUFromSection(path.join(projectRoot, WU_PATHS.STATUS()), error.wuId, '## In Progress');
|
|
301
|
-
return { success: true };
|
|
302
|
-
case CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION:
|
|
303
|
-
await removeWUFromSection(path.join(projectRoot, WU_PATHS.BACKLOG()), error.wuId, '## 🔧 In progress');
|
|
304
|
-
return { success: true };
|
|
305
412
|
case CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE:
|
|
306
413
|
return await removeOrphanWorktree(error.wuId, error.lane, projectRoot);
|
|
307
|
-
case CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE:
|
|
308
|
-
await updateYamlToDone(error.wuId, projectRoot);
|
|
309
|
-
return { success: true };
|
|
310
414
|
default:
|
|
311
|
-
return { skipped: true, reason: `Unknown error type: ${error.type}` };
|
|
415
|
+
return { skipped: true, reason: `Unknown git-only error type: ${error.type}` };
|
|
312
416
|
}
|
|
313
417
|
}
|
|
314
418
|
/**
|
|
315
|
-
* Create stamp file
|
|
419
|
+
* Create stamp file inside a micro-worktree (WU-1078)
|
|
420
|
+
*
|
|
421
|
+
* @param {string} id - WU ID
|
|
422
|
+
* @param {string} title - WU title
|
|
423
|
+
* @param {string} worktreePath - Path to the micro-worktree
|
|
424
|
+
* @returns {Promise<string[]>} List of files created (relative paths)
|
|
425
|
+
*/
|
|
426
|
+
async function createStampInWorktree(id, title, worktreePath) {
|
|
427
|
+
const stampsDir = path.join(worktreePath, WU_PATHS.STAMPS_DIR());
|
|
428
|
+
const stampRelPath = WU_PATHS.STAMP(id);
|
|
429
|
+
const stampAbsPath = path.join(worktreePath, stampRelPath);
|
|
430
|
+
// Ensure stamps directory exists
|
|
431
|
+
if (!existsSync(stampsDir)) {
|
|
432
|
+
mkdirSync(stampsDir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
// Don't overwrite existing stamp
|
|
435
|
+
if (existsSync(stampAbsPath)) {
|
|
436
|
+
return []; // Stamp already exists
|
|
437
|
+
}
|
|
438
|
+
// Create stamp file
|
|
439
|
+
const body = `WU ${id} — ${title}\nCompleted: ${todayISO()}\n`;
|
|
440
|
+
writeFileSync(stampAbsPath, body, { encoding: 'utf-8' });
|
|
441
|
+
return [stampRelPath];
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Create stamp file in a specific project root (DEPRECATED - use createStampInWorktree)
|
|
445
|
+
*
|
|
446
|
+
* Kept for backwards compatibility with code that doesn't use micro-worktree.
|
|
316
447
|
*
|
|
317
448
|
* @param {string} id - WU ID
|
|
318
449
|
* @param {string} title - WU title
|
|
@@ -342,7 +473,7 @@ async function createStampInProject(id, title, projectRoot) {
|
|
|
342
473
|
await writeFile(stampPath, body, { encoding: 'utf-8' });
|
|
343
474
|
}
|
|
344
475
|
/**
|
|
345
|
-
* Update WU YAML to done+locked+completed state (WU-
|
|
476
|
+
* Update WU YAML to done+locked+completed state inside a micro-worktree (WU-1078)
|
|
346
477
|
*
|
|
347
478
|
* Repairs STAMP_EXISTS_YAML_NOT_DONE by setting:
|
|
348
479
|
* - status: done
|
|
@@ -350,6 +481,43 @@ async function createStampInProject(id, title, projectRoot) {
|
|
|
350
481
|
* - completed: YYYY-MM-DD (today, unless already set)
|
|
351
482
|
*
|
|
352
483
|
* @param {string} id - WU ID
|
|
484
|
+
* @param {string} worktreePath - Path to the micro-worktree
|
|
485
|
+
* @param {string} projectRoot - Original project root (for reading source file)
|
|
486
|
+
* @returns {Promise<string[]>} List of files modified (relative paths)
|
|
487
|
+
*/
|
|
488
|
+
async function updateYamlToDoneInWorktree(id, worktreePath, projectRoot) {
|
|
489
|
+
const wuRelPath = WU_PATHS.WU(id);
|
|
490
|
+
const wuSrcPath = path.join(projectRoot, wuRelPath);
|
|
491
|
+
const wuDestPath = path.join(worktreePath, wuRelPath);
|
|
492
|
+
// Read current YAML from project root
|
|
493
|
+
const content = readFileSync(wuSrcPath, { encoding: 'utf-8' });
|
|
494
|
+
const wuDoc = parseYAML(content);
|
|
495
|
+
if (!wuDoc) {
|
|
496
|
+
throw new Error(`Failed to parse WU YAML: ${wuSrcPath}`);
|
|
497
|
+
}
|
|
498
|
+
// Update fields
|
|
499
|
+
wuDoc.status = WU_STATUS.DONE;
|
|
500
|
+
wuDoc.locked = true;
|
|
501
|
+
// Preserve existing completed date if present, otherwise set to today
|
|
502
|
+
if (!wuDoc.completed) {
|
|
503
|
+
wuDoc.completed = todayISO();
|
|
504
|
+
}
|
|
505
|
+
// Ensure directory exists in worktree
|
|
506
|
+
const wuDir = path.dirname(wuDestPath);
|
|
507
|
+
if (!existsSync(wuDir)) {
|
|
508
|
+
mkdirSync(wuDir, { recursive: true });
|
|
509
|
+
}
|
|
510
|
+
// Write updated YAML to worktree
|
|
511
|
+
const updatedContent = stringifyYAML(wuDoc, { lineWidth: YAML_OPTIONS.LINE_WIDTH });
|
|
512
|
+
writeFileSync(wuDestPath, updatedContent, { encoding: 'utf-8' });
|
|
513
|
+
return [wuRelPath];
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Update WU YAML to done+locked+completed state (DEPRECATED - use updateYamlToDoneInWorktree)
|
|
517
|
+
*
|
|
518
|
+
* Kept for backwards compatibility.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} id - WU ID
|
|
353
521
|
* @param {string} projectRoot - Project root directory
|
|
354
522
|
* @returns {Promise<void>}
|
|
355
523
|
*/
|
|
@@ -373,7 +541,69 @@ async function updateYamlToDone(id, projectRoot) {
|
|
|
373
541
|
await writeFile(wuPath, updatedContent, { encoding: 'utf-8' });
|
|
374
542
|
}
|
|
375
543
|
/**
|
|
376
|
-
* Remove WU entry from a specific section in a markdown file
|
|
544
|
+
* Remove WU entry from a specific section in a markdown file inside a micro-worktree (WU-1078)
|
|
545
|
+
*
|
|
546
|
+
* @param {string} relFilePath - Relative path to the markdown file
|
|
547
|
+
* @param {string} id - WU ID to remove
|
|
548
|
+
* @param {string} sectionHeading - Section heading to target
|
|
549
|
+
* @param {string} worktreePath - Path to the micro-worktree
|
|
550
|
+
* @param {string} projectRoot - Original project root (for reading source file)
|
|
551
|
+
* @returns {Promise<string[]>} List of files modified (relative paths)
|
|
552
|
+
*/
|
|
553
|
+
async function removeWUFromSectionInWorktree(relFilePath, id, sectionHeading, worktreePath, projectRoot) {
|
|
554
|
+
const srcPath = path.join(projectRoot, relFilePath);
|
|
555
|
+
const destPath = path.join(worktreePath, relFilePath);
|
|
556
|
+
// Check if source file exists
|
|
557
|
+
if (!existsSync(srcPath)) {
|
|
558
|
+
return []; // File doesn't exist
|
|
559
|
+
}
|
|
560
|
+
const content = readFileSync(srcPath, { encoding: 'utf-8' });
|
|
561
|
+
const lines = content.split(/\r?\n/);
|
|
562
|
+
let inTargetSection = false;
|
|
563
|
+
let nextSectionIdx = -1;
|
|
564
|
+
let sectionStartIdx = -1;
|
|
565
|
+
// Normalize heading for comparison (lowercase, trim)
|
|
566
|
+
const normalizedHeading = sectionHeading.toLowerCase().trim();
|
|
567
|
+
// Find section boundaries
|
|
568
|
+
for (let i = 0; i < lines.length; i++) {
|
|
569
|
+
const normalizedLine = lines[i].toLowerCase().trim();
|
|
570
|
+
if (normalizedLine === normalizedHeading || normalizedLine.startsWith(normalizedHeading)) {
|
|
571
|
+
inTargetSection = true;
|
|
572
|
+
sectionStartIdx = i;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (inTargetSection && lines[i].trim().startsWith('## ')) {
|
|
576
|
+
nextSectionIdx = i;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (sectionStartIdx === -1)
|
|
581
|
+
return [];
|
|
582
|
+
const endIdx = nextSectionIdx === -1 ? lines.length : nextSectionIdx;
|
|
583
|
+
// Filter out lines containing the WU ID in the target section
|
|
584
|
+
const newLines = [];
|
|
585
|
+
let modified = false;
|
|
586
|
+
for (let i = 0; i < lines.length; i++) {
|
|
587
|
+
if (i > sectionStartIdx && i < endIdx && lines[i].includes(id)) {
|
|
588
|
+
modified = true;
|
|
589
|
+
continue; // Skip this line
|
|
590
|
+
}
|
|
591
|
+
newLines.push(lines[i]);
|
|
592
|
+
}
|
|
593
|
+
if (!modified)
|
|
594
|
+
return [];
|
|
595
|
+
// Ensure directory exists in worktree
|
|
596
|
+
const destDir = path.dirname(destPath);
|
|
597
|
+
if (!existsSync(destDir)) {
|
|
598
|
+
mkdirSync(destDir, { recursive: true });
|
|
599
|
+
}
|
|
600
|
+
writeFileSync(destPath, newLines.join(STRING_LITERALS.NEWLINE), { encoding: 'utf-8' });
|
|
601
|
+
return [relFilePath];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Remove WU entry from a specific section in a markdown file (DEPRECATED)
|
|
605
|
+
*
|
|
606
|
+
* Kept for backwards compatibility.
|
|
377
607
|
*
|
|
378
608
|
* @param {string} filePath - Path to the markdown file
|
|
379
609
|
* @param {string} id - WU ID to remove
|
|
@@ -17,8 +17,9 @@ import { z } from 'zod';
|
|
|
17
17
|
* - complete: WU completed (transitions to done)
|
|
18
18
|
* - checkpoint: Progress checkpoint (WU-1748: cross-agent visibility)
|
|
19
19
|
* - spawn: WU spawned from parent (WU-1947: parent-child relationships)
|
|
20
|
+
* - release: WU released (WU-1080: transitions from in_progress to ready for orphan recovery)
|
|
20
21
|
*/
|
|
21
|
-
export declare const WU_EVENT_TYPES: readonly ["create", "claim", "block", "unblock", "complete", "checkpoint", "spawn"];
|
|
22
|
+
export declare const WU_EVENT_TYPES: readonly ["create", "claim", "block", "unblock", "complete", "checkpoint", "spawn", "release"];
|
|
22
23
|
/** Type for WU event types */
|
|
23
24
|
export type WUEventType = (typeof WU_EVENT_TYPES)[number];
|
|
24
25
|
/**
|
|
@@ -103,6 +104,17 @@ export declare const SpawnEventSchema: z.ZodObject<{
|
|
|
103
104
|
parentWuId: z.ZodString;
|
|
104
105
|
spawnId: z.ZodString;
|
|
105
106
|
}, z.core.$strip>;
|
|
107
|
+
/**
|
|
108
|
+
* Release event schema (WU-1080: orphan recovery)
|
|
109
|
+
* Releases an in_progress WU back to ready state when agent is interrupted.
|
|
110
|
+
* Allows another agent to reclaim the orphaned WU.
|
|
111
|
+
*/
|
|
112
|
+
export declare const ReleaseEventSchema: z.ZodObject<{
|
|
113
|
+
wuId: z.ZodString;
|
|
114
|
+
timestamp: z.ZodString;
|
|
115
|
+
type: z.ZodLiteral<"release">;
|
|
116
|
+
reason: z.ZodString;
|
|
117
|
+
}, z.core.$strip>;
|
|
106
118
|
/**
|
|
107
119
|
* Union schema for all event types
|
|
108
120
|
*/
|
|
@@ -145,6 +157,11 @@ export declare const WUEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
145
157
|
type: z.ZodLiteral<"spawn">;
|
|
146
158
|
parentWuId: z.ZodString;
|
|
147
159
|
spawnId: z.ZodString;
|
|
160
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
161
|
+
wuId: z.ZodString;
|
|
162
|
+
timestamp: z.ZodString;
|
|
163
|
+
type: z.ZodLiteral<"release">;
|
|
164
|
+
reason: z.ZodString;
|
|
148
165
|
}, z.core.$strip>], "type">;
|
|
149
166
|
/**
|
|
150
167
|
* TypeScript types inferred from schemas
|
|
@@ -156,6 +173,7 @@ export type UnblockEvent = z.infer<typeof UnblockEventSchema>;
|
|
|
156
173
|
export type CompleteEvent = z.infer<typeof CompleteEventSchema>;
|
|
157
174
|
export type CheckpointEvent = z.infer<typeof CheckpointEventSchema>;
|
|
158
175
|
export type SpawnEvent = z.infer<typeof SpawnEventSchema>;
|
|
176
|
+
export type ReleaseEvent = z.infer<typeof ReleaseEventSchema>;
|
|
159
177
|
export type WUEvent = z.infer<typeof WUEventSchema>;
|
|
160
178
|
/**
|
|
161
179
|
* Validates WU event data against schema
|
|
@@ -210,4 +228,9 @@ export declare function validateWUEvent(data: any): z.ZodSafeParseResult<{
|
|
|
210
228
|
type: "spawn";
|
|
211
229
|
parentWuId: string;
|
|
212
230
|
spawnId: string;
|
|
231
|
+
} | {
|
|
232
|
+
wuId: string;
|
|
233
|
+
timestamp: string;
|
|
234
|
+
type: "release";
|
|
235
|
+
reason: string;
|
|
213
236
|
}>;
|
package/dist/wu-state-schema.js
CHANGED
|
@@ -17,6 +17,7 @@ import { z } from 'zod';
|
|
|
17
17
|
* - complete: WU completed (transitions to done)
|
|
18
18
|
* - checkpoint: Progress checkpoint (WU-1748: cross-agent visibility)
|
|
19
19
|
* - spawn: WU spawned from parent (WU-1947: parent-child relationships)
|
|
20
|
+
* - release: WU released (WU-1080: transitions from in_progress to ready for orphan recovery)
|
|
20
21
|
*/
|
|
21
22
|
export const WU_EVENT_TYPES = [
|
|
22
23
|
'create',
|
|
@@ -26,6 +27,7 @@ export const WU_EVENT_TYPES = [
|
|
|
26
27
|
'complete',
|
|
27
28
|
'checkpoint',
|
|
28
29
|
'spawn',
|
|
30
|
+
'release',
|
|
29
31
|
];
|
|
30
32
|
/**
|
|
31
33
|
* WU status values (matches LumenFlow state machine)
|
|
@@ -125,6 +127,16 @@ export const SpawnEventSchema = BaseEventSchema.extend({
|
|
|
125
127
|
/** Unique spawn identifier */
|
|
126
128
|
spawnId: z.string().min(1, { message: 'Spawn ID is required' }),
|
|
127
129
|
});
|
|
130
|
+
/**
|
|
131
|
+
* Release event schema (WU-1080: orphan recovery)
|
|
132
|
+
* Releases an in_progress WU back to ready state when agent is interrupted.
|
|
133
|
+
* Allows another agent to reclaim the orphaned WU.
|
|
134
|
+
*/
|
|
135
|
+
export const ReleaseEventSchema = BaseEventSchema.extend({
|
|
136
|
+
type: z.literal('release'),
|
|
137
|
+
/** Reason for releasing the WU */
|
|
138
|
+
reason: z.string().min(1, { message: ERROR_MESSAGES.REASON_REQUIRED }),
|
|
139
|
+
});
|
|
128
140
|
/**
|
|
129
141
|
* Union schema for all event types
|
|
130
142
|
*/
|
|
@@ -136,6 +148,7 @@ export const WUEventSchema = z.discriminatedUnion('type', [
|
|
|
136
148
|
CompleteEventSchema,
|
|
137
149
|
CheckpointEventSchema,
|
|
138
150
|
SpawnEventSchema,
|
|
151
|
+
ReleaseEventSchema,
|
|
139
152
|
]);
|
|
140
153
|
/**
|
|
141
154
|
* Validates WU event data against schema
|
package/dist/wu-state-store.d.ts
CHANGED
|
@@ -217,6 +217,27 @@ export declare class WUStateStore {
|
|
|
217
217
|
* await store.spawn('WU-200', 'WU-100', 'spawn-abc123');
|
|
218
218
|
*/
|
|
219
219
|
spawn(childWuId: string, parentWuId: string, spawnId: string): Promise<void>;
|
|
220
|
+
/**
|
|
221
|
+
* Releases an in_progress WU back to ready state (WU-1080: orphan recovery).
|
|
222
|
+
*
|
|
223
|
+
* Use this when an agent is interrupted mid-WU and the WU needs to be
|
|
224
|
+
* made available for reclaiming by another agent.
|
|
225
|
+
*
|
|
226
|
+
* @throws Error If WU is not in_progress
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* await store.release('WU-1080', 'Agent interrupted mid-WU');
|
|
230
|
+
*/
|
|
231
|
+
release(wuId: string, reason: string): Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Create a release event without writing to disk.
|
|
234
|
+
*
|
|
235
|
+
* Used by transactional flows where event log writes are staged and committed atomically.
|
|
236
|
+
* WU-1080: Orphan recovery support.
|
|
237
|
+
*
|
|
238
|
+
* @throws Error If WU is not in_progress or event fails validation
|
|
239
|
+
*/
|
|
240
|
+
createReleaseEvent(wuId: string, reason: string, timestamp?: string): WUEvent;
|
|
220
241
|
}
|
|
221
242
|
/**
|
|
222
243
|
* Check if a lock is stale (expired or dead process)
|
package/dist/wu-state-store.js
CHANGED
|
@@ -171,6 +171,11 @@ export class WUStateStore {
|
|
|
171
171
|
this.byParent.set(parentWuId, new Set());
|
|
172
172
|
}
|
|
173
173
|
this.byParent.get(parentWuId).add(wuId);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// WU-1080: Handle release event - transitions from in_progress to ready
|
|
177
|
+
if (type === 'release') {
|
|
178
|
+
this._transitionToStatus(wuId, 'ready');
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
/**
|
|
@@ -446,6 +451,55 @@ export class WUStateStore {
|
|
|
446
451
|
await this._appendEvent(event);
|
|
447
452
|
this._applyEvent(event);
|
|
448
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Releases an in_progress WU back to ready state (WU-1080: orphan recovery).
|
|
456
|
+
*
|
|
457
|
+
* Use this when an agent is interrupted mid-WU and the WU needs to be
|
|
458
|
+
* made available for reclaiming by another agent.
|
|
459
|
+
*
|
|
460
|
+
* @throws Error If WU is not in_progress
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* await store.release('WU-1080', 'Agent interrupted mid-WU');
|
|
464
|
+
*/
|
|
465
|
+
async release(wuId, reason) {
|
|
466
|
+
// Check state machine: can only release if in_progress
|
|
467
|
+
const currentState = this.wuState.get(wuId);
|
|
468
|
+
if (!currentState || currentState.status !== 'in_progress') {
|
|
469
|
+
throw new Error(`WU ${wuId} is not in_progress`);
|
|
470
|
+
}
|
|
471
|
+
const event = {
|
|
472
|
+
type: 'release',
|
|
473
|
+
wuId,
|
|
474
|
+
reason,
|
|
475
|
+
timestamp: new Date().toISOString(),
|
|
476
|
+
};
|
|
477
|
+
await this._appendEvent(event);
|
|
478
|
+
this._applyEvent(event);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Create a release event without writing to disk.
|
|
482
|
+
*
|
|
483
|
+
* Used by transactional flows where event log writes are staged and committed atomically.
|
|
484
|
+
* WU-1080: Orphan recovery support.
|
|
485
|
+
*
|
|
486
|
+
* @throws Error If WU is not in_progress or event fails validation
|
|
487
|
+
*/
|
|
488
|
+
createReleaseEvent(wuId, reason, timestamp = new Date().toISOString()) {
|
|
489
|
+
const currentState = this.wuState.get(wuId);
|
|
490
|
+
if (!currentState || currentState.status !== 'in_progress') {
|
|
491
|
+
throw new Error(`WU ${wuId} is not in_progress`);
|
|
492
|
+
}
|
|
493
|
+
const event = { type: 'release', wuId, reason, timestamp };
|
|
494
|
+
const validation = validateWUEvent(event);
|
|
495
|
+
if (!validation.success) {
|
|
496
|
+
const issues = validation.error.issues
|
|
497
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
498
|
+
.join(', ');
|
|
499
|
+
throw new Error(`Validation error: ${issues}`);
|
|
500
|
+
}
|
|
501
|
+
return validation.data;
|
|
502
|
+
}
|
|
449
503
|
}
|
|
450
504
|
/**
|
|
451
505
|
* Check if a process with given PID is running
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumenflow/core",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
95
|
-
"@lumenflow/initiatives": "1.
|
|
94
|
+
"@lumenflow/memory": "1.4.0",
|
|
95
|
+
"@lumenflow/initiatives": "1.4.0"
|
|
96
96
|
},
|
|
97
97
|
"peerDependenciesMeta": {
|
|
98
98
|
"@lumenflow/memory": {
|