@lumenflow/mcp 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +16 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli-runner.d.ts +58 -0
- package/dist/cli-runner.d.ts.map +1 -0
- package/dist/cli-runner.js +164 -0
- package/dist/cli-runner.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-constants.d.ts +177 -0
- package/dist/mcp-constants.d.ts.map +1 -0
- package/dist/mcp-constants.js +197 -0
- package/dist/mcp-constants.js.map +1 -0
- package/dist/resources.d.ts +53 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +131 -0
- package/dist/resources.js.map +1 -0
- package/dist/runtime-cache.d.ts +7 -0
- package/dist/runtime-cache.d.ts.map +1 -0
- package/dist/runtime-cache.js +28 -0
- package/dist/runtime-cache.js.map +1 -0
- package/dist/runtime-tool-resolver.constants.d.ts +26 -0
- package/dist/runtime-tool-resolver.constants.d.ts.map +1 -0
- package/dist/runtime-tool-resolver.constants.js +36 -0
- package/dist/runtime-tool-resolver.constants.js.map +1 -0
- package/dist/runtime-tool-resolver.d.ts +5 -0
- package/dist/runtime-tool-resolver.d.ts.map +1 -0
- package/dist/runtime-tool-resolver.js +2030 -0
- package/dist/runtime-tool-resolver.js.map +1 -0
- package/dist/server.d.ts +58 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/agent-tools.d.ts +18 -0
- package/dist/tools/agent-tools.d.ts.map +1 -0
- package/dist/tools/agent-tools.js +235 -0
- package/dist/tools/agent-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +13 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +58 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/flow-tools.d.ts +22 -0
- package/dist/tools/flow-tools.d.ts.map +1 -0
- package/dist/tools/flow-tools.js +130 -0
- package/dist/tools/flow-tools.js.map +1 -0
- package/dist/tools/initiative-tools.d.ts +34 -0
- package/dist/tools/initiative-tools.d.ts.map +1 -0
- package/dist/tools/initiative-tools.js +420 -0
- package/dist/tools/initiative-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +58 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +523 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/orchestration-tools.d.ts +18 -0
- package/dist/tools/orchestration-tools.d.ts.map +1 -0
- package/dist/tools/orchestration-tools.js +202 -0
- package/dist/tools/orchestration-tools.js.map +1 -0
- package/dist/tools/parity-tools.d.ts +138 -0
- package/dist/tools/parity-tools.d.ts.map +1 -0
- package/dist/tools/parity-tools.js +1690 -0
- package/dist/tools/parity-tools.js.map +1 -0
- package/dist/tools/runtime-task-constants.d.ts +19 -0
- package/dist/tools/runtime-task-constants.d.ts.map +1 -0
- package/dist/tools/runtime-task-constants.js +21 -0
- package/dist/tools/runtime-task-constants.js.map +1 -0
- package/dist/tools/runtime-task-tools.d.ts +10 -0
- package/dist/tools/runtime-task-tools.d.ts.map +1 -0
- package/dist/tools/runtime-task-tools.js +116 -0
- package/dist/tools/runtime-task-tools.js.map +1 -0
- package/dist/tools/setup-tools.d.ts +34 -0
- package/dist/tools/setup-tools.d.ts.map +1 -0
- package/dist/tools/setup-tools.js +254 -0
- package/dist/tools/setup-tools.js.map +1 -0
- package/dist/tools/validation-tools.d.ts +26 -0
- package/dist/tools/validation-tools.d.ts.map +1 -0
- package/dist/tools/validation-tools.js +180 -0
- package/dist/tools/validation-tools.js.map +1 -0
- package/dist/tools/wu-tools.d.ts +101 -0
- package/dist/tools/wu-tools.d.ts.map +1 -0
- package/dist/tools/wu-tools.js +964 -0
- package/dist/tools/wu-tools.js.map +1 -0
- package/dist/tools-shared.d.ts +257 -0
- package/dist/tools-shared.d.ts.map +1 -0
- package/dist/tools-shared.js +410 -0
- package/dist/tools-shared.js.map +1 -0
- package/dist/tools.d.ts +99 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +253 -0
- package/dist/tools.js.map +1 -0
- package/dist/worktree-enforcement.d.ts +32 -0
- package/dist/worktree-enforcement.d.ts.map +1 -0
- package/dist/worktree-enforcement.js +154 -0
- package/dist/worktree-enforcement.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { TOOL_HANDLER_KINDS, defaultRuntimeToolCapabilityResolver, } from '@lumenflow/kernel';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { STATE_RUNTIME_CONSTANTS, STATE_RUNTIME_EVENT_TYPES, STATE_RUNTIME_MESSAGES, } from './runtime-tool-resolver.constants.js';
|
|
8
|
+
import { MetadataKeys } from './mcp-constants.js';
|
|
9
|
+
const DEFAULT_IN_PROCESS_INPUT_SCHEMA = z.record(z.string(), z.unknown());
|
|
10
|
+
const DEFAULT_IN_PROCESS_OUTPUT_SCHEMA = z.record(z.string(), z.unknown());
|
|
11
|
+
const RUNTIME_PROJECT_ROOT_METADATA_KEY = MetadataKeys.PROJECT_ROOT;
|
|
12
|
+
const UTF8_ENCODING = 'utf-8';
|
|
13
|
+
const DEFAULT_FILE_READ_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
14
|
+
const ORCHESTRATION_TOOL_ERROR_CODES = {
|
|
15
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
16
|
+
ORCHESTRATE_INIT_STATUS_ERROR: 'ORCHESTRATE_INIT_STATUS_ERROR',
|
|
17
|
+
ORCHESTRATE_MONITOR_ERROR: 'ORCHESTRATE_MONITOR_ERROR',
|
|
18
|
+
DELEGATION_LIST_ERROR: 'DELEGATION_LIST_ERROR',
|
|
19
|
+
};
|
|
20
|
+
const ORCHESTRATE_MONITOR_DEFAULT_SINCE = '30m';
|
|
21
|
+
const ORCHESTRATE_MONITOR_TIME_PATTERN = /^(\d+)\s*([smhd])$/i;
|
|
22
|
+
const ORCHESTRATE_MONITOR_TIME_MULTIPLIERS = {
|
|
23
|
+
s: 1000,
|
|
24
|
+
m: 60_000,
|
|
25
|
+
h: 3_600_000,
|
|
26
|
+
d: 86_400_000,
|
|
27
|
+
};
|
|
28
|
+
const INITIATIVE_FILE_SUFFIX = '.yaml';
|
|
29
|
+
const STATUS_DONE = 'done';
|
|
30
|
+
const STATUS_IN_PROGRESS = 'in_progress';
|
|
31
|
+
const STATUS_BLOCKED = 'blocked';
|
|
32
|
+
const STATUS_READY = 'ready';
|
|
33
|
+
const STATUS_UNKNOWN = 'unknown';
|
|
34
|
+
const LOCK_POLICY_ALL = 'all';
|
|
35
|
+
const LOCK_POLICY_ACTIVE = 'active';
|
|
36
|
+
const LOCK_POLICY_NONE = 'none';
|
|
37
|
+
const DEFAULT_WIP_LIMIT = 1;
|
|
38
|
+
const DELEGATION_LIST_LOG_PREFIX = '[delegation:list]';
|
|
39
|
+
const INIT_STATUS_HEADER = 'Initiative:';
|
|
40
|
+
const INIT_STATUS_PROGRESS_HEADER = 'Progress:';
|
|
41
|
+
const INIT_STATUS_WUS_HEADER = 'WUs:';
|
|
42
|
+
const INIT_STATUS_LANE_HEADER = 'Lane Availability:';
|
|
43
|
+
const LANE_SECTION_KEYS = ['definitions', 'engineering', 'business'];
|
|
44
|
+
const DEFAULT_SIGNAL_TYPE = 'unknown';
|
|
45
|
+
const WU_ID_PATTERN = /^WU-\d+$/;
|
|
46
|
+
const FILE_TOOL_ERROR_CODES = {
|
|
47
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
48
|
+
FILE_READ_FAILED: 'FILE_READ_FAILED',
|
|
49
|
+
FILE_READ_TOO_LARGE: 'FILE_READ_TOO_LARGE',
|
|
50
|
+
FILE_WRITE_FAILED: 'FILE_WRITE_FAILED',
|
|
51
|
+
FILE_EDIT_FAILED: 'FILE_EDIT_FAILED',
|
|
52
|
+
FILE_EDIT_TARGET_NOT_FOUND: 'FILE_EDIT_TARGET_NOT_FOUND',
|
|
53
|
+
FILE_EDIT_NOT_UNIQUE: 'FILE_EDIT_NOT_UNIQUE',
|
|
54
|
+
FILE_DELETE_FAILED: 'FILE_DELETE_FAILED',
|
|
55
|
+
};
|
|
56
|
+
const FILE_TOOL_MESSAGES = {
|
|
57
|
+
FILE_WRITTEN: 'File written',
|
|
58
|
+
FILE_EDITED: 'File edited',
|
|
59
|
+
DELETE_COMPLETE: 'Delete complete',
|
|
60
|
+
PATH_NOT_FOUND: 'Path not found',
|
|
61
|
+
PARENT_DIRECTORY_MISSING: 'Parent directory does not exist',
|
|
62
|
+
DIRECTORY_NOT_EMPTY: 'Directory is not empty. Use recursive=true to delete non-empty directories.',
|
|
63
|
+
};
|
|
64
|
+
const STATE_TOOL_ERROR_CODES = {
|
|
65
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
66
|
+
BACKLOG_PRUNE_FAILED: 'BACKLOG_PRUNE_FAILED',
|
|
67
|
+
STATE_BOOTSTRAP_FAILED: 'STATE_BOOTSTRAP_FAILED',
|
|
68
|
+
STATE_CLEANUP_FAILED: 'STATE_CLEANUP_FAILED',
|
|
69
|
+
STATE_DOCTOR_FAILED: 'STATE_DOCTOR_FAILED',
|
|
70
|
+
SIGNAL_CLEANUP_FAILED: 'SIGNAL_CLEANUP_FAILED',
|
|
71
|
+
};
|
|
72
|
+
const FILE_READ_INPUT_SCHEMA = z.object({
|
|
73
|
+
path: z.string().min(1),
|
|
74
|
+
encoding: z.string().optional(),
|
|
75
|
+
start_line: z.number().int().positive().optional(),
|
|
76
|
+
end_line: z.number().int().positive().optional(),
|
|
77
|
+
max_size: z.number().int().positive().optional(),
|
|
78
|
+
});
|
|
79
|
+
const FILE_WRITE_INPUT_SCHEMA = z.object({
|
|
80
|
+
path: z.string().min(1),
|
|
81
|
+
content: z.string(),
|
|
82
|
+
encoding: z.string().optional(),
|
|
83
|
+
no_create_dirs: z.boolean().optional(),
|
|
84
|
+
});
|
|
85
|
+
const FILE_EDIT_INPUT_SCHEMA = z.object({
|
|
86
|
+
path: z.string().min(1),
|
|
87
|
+
old_string: z.string(),
|
|
88
|
+
new_string: z.string(),
|
|
89
|
+
encoding: z.string().optional(),
|
|
90
|
+
replace_all: z.boolean().optional(),
|
|
91
|
+
});
|
|
92
|
+
const FILE_DELETE_INPUT_SCHEMA = z.object({
|
|
93
|
+
path: z.string().min(1),
|
|
94
|
+
recursive: z.boolean().optional(),
|
|
95
|
+
force: z.boolean().optional(),
|
|
96
|
+
});
|
|
97
|
+
const BACKLOG_PRUNE_INPUT_SCHEMA = z.object({
|
|
98
|
+
execute: z.boolean().optional(),
|
|
99
|
+
dry_run: z.boolean().optional(),
|
|
100
|
+
stale_days_in_progress: z.number().int().positive().optional(),
|
|
101
|
+
stale_days_ready: z.number().int().positive().optional(),
|
|
102
|
+
archive_days: z.number().int().positive().optional(),
|
|
103
|
+
});
|
|
104
|
+
const STATE_BOOTSTRAP_INPUT_SCHEMA = z.object({
|
|
105
|
+
execute: z.boolean().optional(),
|
|
106
|
+
dry_run: z.boolean().optional(),
|
|
107
|
+
force: z.boolean().optional(),
|
|
108
|
+
wu_dir: z.string().optional(),
|
|
109
|
+
state_dir: z.string().optional(),
|
|
110
|
+
});
|
|
111
|
+
const STATE_CLEANUP_INPUT_SCHEMA = z.object({
|
|
112
|
+
dry_run: z.boolean().optional(),
|
|
113
|
+
signals_only: z.boolean().optional(),
|
|
114
|
+
memory_only: z.boolean().optional(),
|
|
115
|
+
events_only: z.boolean().optional(),
|
|
116
|
+
json: z.boolean().optional(),
|
|
117
|
+
quiet: z.boolean().optional(),
|
|
118
|
+
base_dir: z.string().optional(),
|
|
119
|
+
});
|
|
120
|
+
const STATE_DOCTOR_INPUT_SCHEMA = z.object({
|
|
121
|
+
fix: z.boolean().optional(),
|
|
122
|
+
dry_run: z.boolean().optional(),
|
|
123
|
+
json: z.boolean().optional(),
|
|
124
|
+
quiet: z.boolean().optional(),
|
|
125
|
+
base_dir: z.string().optional(),
|
|
126
|
+
});
|
|
127
|
+
const SIGNAL_CLEANUP_INPUT_SCHEMA = z.object({
|
|
128
|
+
dry_run: z.boolean().optional(),
|
|
129
|
+
ttl: z.string().optional(),
|
|
130
|
+
unread_ttl: z.string().optional(),
|
|
131
|
+
max_entries: z.number().int().positive().optional(),
|
|
132
|
+
json: z.boolean().optional(),
|
|
133
|
+
quiet: z.boolean().optional(),
|
|
134
|
+
base_dir: z.string().optional(),
|
|
135
|
+
});
|
|
136
|
+
const ORCHESTRATE_INIT_STATUS_INPUT_SCHEMA = z.object({
|
|
137
|
+
initiative: z.string().min(1),
|
|
138
|
+
});
|
|
139
|
+
const ORCHESTRATE_MONITOR_INPUT_SCHEMA = z.object({
|
|
140
|
+
threshold: z.number().positive().optional(),
|
|
141
|
+
recover: z.boolean().optional(),
|
|
142
|
+
dry_run: z.boolean().optional(),
|
|
143
|
+
since: z.string().optional(),
|
|
144
|
+
wu: z.string().optional(),
|
|
145
|
+
signals_only: z.boolean().optional(),
|
|
146
|
+
});
|
|
147
|
+
const DELEGATION_LIST_INPUT_SCHEMA = z.object({
|
|
148
|
+
wu: z.string().optional(),
|
|
149
|
+
initiative: z.string().optional(),
|
|
150
|
+
json: z.boolean().optional(),
|
|
151
|
+
});
|
|
152
|
+
const FILE_READ_OUTPUT_SCHEMA = z.object({
|
|
153
|
+
content: z.string(),
|
|
154
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
155
|
+
});
|
|
156
|
+
const FILE_WRITE_OUTPUT_SCHEMA = z.object({
|
|
157
|
+
message: z.string(),
|
|
158
|
+
path: z.string(),
|
|
159
|
+
bytes_written: z.number().int().nonnegative(),
|
|
160
|
+
});
|
|
161
|
+
const FILE_EDIT_OUTPUT_SCHEMA = z.object({
|
|
162
|
+
message: z.string(),
|
|
163
|
+
path: z.string(),
|
|
164
|
+
replacements: z.number().int().positive(),
|
|
165
|
+
});
|
|
166
|
+
const FILE_DELETE_OUTPUT_SCHEMA = z.object({
|
|
167
|
+
message: z.string(),
|
|
168
|
+
metadata: z
|
|
169
|
+
.object({
|
|
170
|
+
deleted_count: z.number().int().nonnegative(),
|
|
171
|
+
was_directory: z.boolean(),
|
|
172
|
+
})
|
|
173
|
+
.optional(),
|
|
174
|
+
});
|
|
175
|
+
// WU-1803: Lazy module loaders to avoid eager imports (same pattern as getCore in tools-shared)
|
|
176
|
+
let coreModule = null;
|
|
177
|
+
async function getCoreLazy() {
|
|
178
|
+
if (!coreModule)
|
|
179
|
+
coreModule = await import('@lumenflow/core');
|
|
180
|
+
return coreModule;
|
|
181
|
+
}
|
|
182
|
+
const MEMORY_MODULE_ID = '@lumenflow/memory';
|
|
183
|
+
let memoryModule = null;
|
|
184
|
+
async function getMemoryLazy() {
|
|
185
|
+
if (!memoryModule) {
|
|
186
|
+
memoryModule = (await import(MEMORY_MODULE_ID));
|
|
187
|
+
}
|
|
188
|
+
return memoryModule;
|
|
189
|
+
}
|
|
190
|
+
// --- WU-1803: Context in-process handler implementations ---
|
|
191
|
+
// WU-1905: flow:bottlenecks, flow:report, metrics, and metrics:snapshot handlers
|
|
192
|
+
// have been migrated to pack handler implementations in
|
|
193
|
+
// packages/@lumenflow/packs/software-delivery/tool-impl/flow-metrics-tools.ts
|
|
194
|
+
/**
|
|
195
|
+
* context:get handler — delegates to @lumenflow/core computeWuContext
|
|
196
|
+
*/
|
|
197
|
+
const contextGetHandler = async () => {
|
|
198
|
+
try {
|
|
199
|
+
const core = await getCoreLazy();
|
|
200
|
+
const context = await core.computeWuContext({ cwd: process.cwd() });
|
|
201
|
+
return { success: true, data: context };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: { code: 'CONTEXT_ERROR', message: err.message },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* wu:list handler — delegates to @lumenflow/core listWUs
|
|
212
|
+
*/
|
|
213
|
+
const wuListHandler = async (rawInput) => {
|
|
214
|
+
try {
|
|
215
|
+
const input = (rawInput ?? {});
|
|
216
|
+
const core = await getCoreLazy();
|
|
217
|
+
const options = { projectRoot: process.cwd() };
|
|
218
|
+
if (typeof input.status === 'string')
|
|
219
|
+
options.status = input.status;
|
|
220
|
+
if (typeof input.lane === 'string')
|
|
221
|
+
options.lane = input.lane;
|
|
222
|
+
const wus = await core.listWUs(options);
|
|
223
|
+
return { success: true, data: wus };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: { code: 'WU_LIST_ERROR', message: err.message },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function isRecord(value) {
|
|
233
|
+
return typeof value === 'object' && value !== null;
|
|
234
|
+
}
|
|
235
|
+
function normalizeToken(value) {
|
|
236
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
237
|
+
}
|
|
238
|
+
function normalizeLifecycleStatus(value) {
|
|
239
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
240
|
+
}
|
|
241
|
+
function hasIncompletePhase(phases) {
|
|
242
|
+
if (!Array.isArray(phases) || phases.length === 0) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return phases.some((phase) => {
|
|
246
|
+
if (!isRecord(phase)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return normalizeLifecycleStatus(phase.status) !== STATUS_DONE;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function deriveInitiativeLifecycleStatus(status, phases) {
|
|
253
|
+
const normalizedStatus = normalizeLifecycleStatus(status);
|
|
254
|
+
if (normalizedStatus === STATUS_DONE && hasIncompletePhase(phases)) {
|
|
255
|
+
return STATUS_IN_PROGRESS;
|
|
256
|
+
}
|
|
257
|
+
return normalizedStatus || STATUS_IN_PROGRESS;
|
|
258
|
+
}
|
|
259
|
+
function extractInitiativeWuIds(wus) {
|
|
260
|
+
if (!Array.isArray(wus)) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const wuIds = [];
|
|
264
|
+
for (const entry of wus) {
|
|
265
|
+
if (typeof entry === 'string' && entry.trim().length > 0) {
|
|
266
|
+
wuIds.push(entry);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (isRecord(entry) && typeof entry.id === 'string' && entry.id.trim().length > 0) {
|
|
270
|
+
wuIds.push(entry.id);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return wuIds;
|
|
274
|
+
}
|
|
275
|
+
function countProgress(entries) {
|
|
276
|
+
const progress = {
|
|
277
|
+
total: entries.length,
|
|
278
|
+
done: 0,
|
|
279
|
+
active: 0,
|
|
280
|
+
pending: 0,
|
|
281
|
+
blocked: 0,
|
|
282
|
+
percentage: 0,
|
|
283
|
+
};
|
|
284
|
+
for (const wu of entries) {
|
|
285
|
+
if (wu.status === STATUS_DONE) {
|
|
286
|
+
progress.done += 1;
|
|
287
|
+
}
|
|
288
|
+
else if (wu.status === STATUS_IN_PROGRESS) {
|
|
289
|
+
progress.active += 1;
|
|
290
|
+
}
|
|
291
|
+
else if (wu.status === STATUS_BLOCKED) {
|
|
292
|
+
progress.blocked += 1;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
progress.pending += 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
progress.percentage = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
|
|
299
|
+
return progress;
|
|
300
|
+
}
|
|
301
|
+
function collectLaneDefinitions(value, target) {
|
|
302
|
+
if (!Array.isArray(value)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const entry of value) {
|
|
306
|
+
if (isRecord(entry)) {
|
|
307
|
+
target.push(entry);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function resolveLanePolicyConfig(config) {
|
|
312
|
+
const laneConfigMap = {};
|
|
313
|
+
if (!isRecord(config) || !isRecord(config.lanes)) {
|
|
314
|
+
return laneConfigMap;
|
|
315
|
+
}
|
|
316
|
+
const laneDefinitions = [];
|
|
317
|
+
if (Array.isArray(config.lanes)) {
|
|
318
|
+
collectLaneDefinitions(config.lanes, laneDefinitions);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
for (const key of LANE_SECTION_KEYS) {
|
|
322
|
+
collectLaneDefinitions(config.lanes[key], laneDefinitions);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const laneDefinition of laneDefinitions) {
|
|
326
|
+
if (typeof laneDefinition.name !== 'string' || laneDefinition.name.trim().length === 0) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const lockPolicy = laneDefinition.lock_policy === LOCK_POLICY_ACTIVE ||
|
|
330
|
+
laneDefinition.lock_policy === LOCK_POLICY_NONE
|
|
331
|
+
? laneDefinition.lock_policy
|
|
332
|
+
: LOCK_POLICY_ALL;
|
|
333
|
+
const wipLimit = typeof laneDefinition.wip_limit === 'number' ? laneDefinition.wip_limit : DEFAULT_WIP_LIMIT;
|
|
334
|
+
laneConfigMap[laneDefinition.name] = { lockPolicy, wipLimit };
|
|
335
|
+
}
|
|
336
|
+
return laneConfigMap;
|
|
337
|
+
}
|
|
338
|
+
function computeLaneAvailability(wus, laneConfigMap) {
|
|
339
|
+
const groupedByLane = new Map();
|
|
340
|
+
for (const wu of wus) {
|
|
341
|
+
if (!wu.lane) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const laneEntries = groupedByLane.get(wu.lane);
|
|
345
|
+
if (laneEntries) {
|
|
346
|
+
laneEntries.push(wu);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
groupedByLane.set(wu.lane, [wu]);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const result = {};
|
|
353
|
+
for (const [lane, entries] of groupedByLane) {
|
|
354
|
+
const laneConfig = laneConfigMap[lane] ?? {
|
|
355
|
+
lockPolicy: LOCK_POLICY_ALL,
|
|
356
|
+
wipLimit: DEFAULT_WIP_LIMIT,
|
|
357
|
+
};
|
|
358
|
+
const inProgress = entries.filter((wu) => wu.status === STATUS_IN_PROGRESS);
|
|
359
|
+
const blocked = entries.filter((wu) => wu.status === STATUS_BLOCKED);
|
|
360
|
+
let available;
|
|
361
|
+
let occupiedBy = null;
|
|
362
|
+
if (laneConfig.lockPolicy === LOCK_POLICY_NONE) {
|
|
363
|
+
available = true;
|
|
364
|
+
}
|
|
365
|
+
else if (laneConfig.lockPolicy === LOCK_POLICY_ACTIVE) {
|
|
366
|
+
available = inProgress.length === 0;
|
|
367
|
+
occupiedBy = inProgress[0]?.id ?? null;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
available = inProgress.length === 0 && blocked.length === 0;
|
|
371
|
+
occupiedBy = inProgress[0]?.id ?? blocked[0]?.id ?? null;
|
|
372
|
+
}
|
|
373
|
+
result[lane] = {
|
|
374
|
+
available,
|
|
375
|
+
policy: laneConfig.lockPolicy,
|
|
376
|
+
occupied_by: occupiedBy,
|
|
377
|
+
in_progress: inProgress.length,
|
|
378
|
+
blocked: blocked.length,
|
|
379
|
+
wip_limit: laneConfig.wipLimit,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
function formatInitiativeStatusMessage(input) {
|
|
385
|
+
const lines = [];
|
|
386
|
+
lines.push(`${INIT_STATUS_HEADER} ${input.initiativeId} - ${input.initiativeTitle}`);
|
|
387
|
+
lines.push(`Lifecycle Status: ${input.lifecycleStatus}`);
|
|
388
|
+
if (input.rawStatus && input.rawStatus !== input.lifecycleStatus) {
|
|
389
|
+
lines.push(`Lifecycle mismatch: metadata status '${input.rawStatus}' conflicts with phase state; reporting '${input.lifecycleStatus}'.`);
|
|
390
|
+
}
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push(INIT_STATUS_PROGRESS_HEADER);
|
|
393
|
+
lines.push(` Done: ${input.progress.done}/${input.progress.total} (${input.progress.percentage}%)`);
|
|
394
|
+
lines.push(` Active: ${input.progress.active}`);
|
|
395
|
+
lines.push(` Pending: ${input.progress.pending}`);
|
|
396
|
+
lines.push(` Blocked: ${input.progress.blocked}`);
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push(INIT_STATUS_WUS_HEADER);
|
|
399
|
+
if (input.wus.length === 0) {
|
|
400
|
+
lines.push(' (no WUs found for initiative)');
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
for (const wu of input.wus) {
|
|
404
|
+
lines.push(` ${wu.id}: ${wu.title} [${wu.status}]`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
lines.push('');
|
|
408
|
+
lines.push(INIT_STATUS_LANE_HEADER);
|
|
409
|
+
const lanes = Object.keys(input.laneAvailability).sort((left, right) => left.localeCompare(right));
|
|
410
|
+
if (lanes.length === 0) {
|
|
411
|
+
lines.push(' (no lanes found)');
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
for (const lane of lanes) {
|
|
415
|
+
const availability = input.laneAvailability[lane];
|
|
416
|
+
if (!availability) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const status = availability.available ? 'available' : 'occupied';
|
|
420
|
+
lines.push(` ${lane}: ${status} (wip_limit=${availability.wip_limit}, lock_policy=${availability.policy}, in_progress=${availability.in_progress}, blocked=${availability.blocked}, occupied_by=${availability.occupied_by ?? 'none'})`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return lines.join('\n');
|
|
424
|
+
}
|
|
425
|
+
async function resolveInitiativeDoc(core, projectRoot, initiativeRef) {
|
|
426
|
+
const config = core.getConfig({ projectRoot });
|
|
427
|
+
const initiativesDir = path.join(projectRoot, config.directories.initiativesDir);
|
|
428
|
+
const initiativeFiles = await readdir(initiativesDir);
|
|
429
|
+
const normalizedRef = normalizeToken(initiativeRef);
|
|
430
|
+
for (const file of initiativeFiles) {
|
|
431
|
+
if (!file.endsWith(INITIATIVE_FILE_SUFFIX)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const content = await readFile(path.join(initiativesDir, file), UTF8_ENCODING);
|
|
435
|
+
const parsed = core.parseYAML(content);
|
|
436
|
+
if (!isRecord(parsed)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const id = normalizeToken(parsed.id);
|
|
440
|
+
const slug = normalizeToken(parsed.slug);
|
|
441
|
+
if (id === normalizedRef || slug === normalizedRef) {
|
|
442
|
+
return {
|
|
443
|
+
id: typeof parsed.id === 'string' ? parsed.id : undefined,
|
|
444
|
+
slug: typeof parsed.slug === 'string' ? parsed.slug : undefined,
|
|
445
|
+
title: typeof parsed.title === 'string' ? parsed.title : undefined,
|
|
446
|
+
status: parsed.status,
|
|
447
|
+
phases: parsed.phases,
|
|
448
|
+
wus: parsed.wus,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`Initiative '${initiativeRef}' not found`);
|
|
453
|
+
}
|
|
454
|
+
async function getCompletedWuIdsFromStamps(core, projectRoot) {
|
|
455
|
+
const completed = new Set();
|
|
456
|
+
const stampsPath = path.join(projectRoot, core.LUMENFLOW_PATHS.STAMPS_DIR);
|
|
457
|
+
try {
|
|
458
|
+
const files = await readdir(stampsPath);
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
if (file.endsWith('.done')) {
|
|
461
|
+
completed.add(file.slice(0, -'.done'.length));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return completed;
|
|
467
|
+
}
|
|
468
|
+
return completed;
|
|
469
|
+
}
|
|
470
|
+
function parseSinceInputToDate(sinceInput) {
|
|
471
|
+
const relativeMatch = ORCHESTRATE_MONITOR_TIME_PATTERN.exec(sinceInput.trim());
|
|
472
|
+
if (relativeMatch) {
|
|
473
|
+
const amount = Number.parseInt(relativeMatch[1] ?? '0', 10);
|
|
474
|
+
const unit = (relativeMatch[2] ?? '').toLowerCase();
|
|
475
|
+
const multiplier = ORCHESTRATE_MONITOR_TIME_MULTIPLIERS[unit];
|
|
476
|
+
if (Number.isFinite(amount) && amount > 0 && multiplier) {
|
|
477
|
+
return new Date(Date.now() - amount * multiplier);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const absoluteDate = new Date(sinceInput);
|
|
481
|
+
if (Number.isNaN(absoluteDate.getTime())) {
|
|
482
|
+
throw new Error(`Invalid time format: ${sinceInput}`);
|
|
483
|
+
}
|
|
484
|
+
return absoluteDate;
|
|
485
|
+
}
|
|
486
|
+
async function loadRecentSignals(core, projectRoot, since) {
|
|
487
|
+
const signalRecords = [];
|
|
488
|
+
const memoryPath = path.join(projectRoot, core.LUMENFLOW_PATHS.MEMORY_DIR);
|
|
489
|
+
let files;
|
|
490
|
+
try {
|
|
491
|
+
files = await readdir(memoryPath);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return signalRecords;
|
|
495
|
+
}
|
|
496
|
+
const ndjsonFiles = files.filter((file) => file.endsWith('.ndjson'));
|
|
497
|
+
for (const file of ndjsonFiles) {
|
|
498
|
+
const content = await readFile(path.join(memoryPath, file), UTF8_ENCODING);
|
|
499
|
+
for (const line of content.split('\n')) {
|
|
500
|
+
const trimmedLine = line.trim();
|
|
501
|
+
if (trimmedLine.length === 0) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const parsed = JSON.parse(trimmedLine);
|
|
506
|
+
if (!isRecord(parsed) || typeof parsed.timestamp !== 'string') {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const timestamp = new Date(parsed.timestamp);
|
|
510
|
+
if (Number.isNaN(timestamp.getTime()) || timestamp < since) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
signalRecords.push({
|
|
514
|
+
timestamp: parsed.timestamp,
|
|
515
|
+
type: typeof parsed.type === 'string' ? parsed.type : DEFAULT_SIGNAL_TYPE,
|
|
516
|
+
wuId: typeof parsed.wuId === 'string' ? parsed.wuId : undefined,
|
|
517
|
+
message: typeof parsed.message === 'string' ? parsed.message : undefined,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
signalRecords.sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
|
|
526
|
+
return signalRecords;
|
|
527
|
+
}
|
|
528
|
+
const orchestrateInitStatusInProcess = async (rawInput, context) => {
|
|
529
|
+
const parsedInput = ORCHESTRATE_INIT_STATUS_INPUT_SCHEMA.safeParse(rawInput);
|
|
530
|
+
if (!parsedInput.success) {
|
|
531
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const core = await getCoreLazy();
|
|
535
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
536
|
+
const initiativeDoc = await resolveInitiativeDoc(core, projectRoot, parsedInput.data.initiative);
|
|
537
|
+
const initiativeId = initiativeDoc.id ?? parsedInput.data.initiative;
|
|
538
|
+
const initiativeSlug = initiativeDoc.slug ?? '';
|
|
539
|
+
const allWUs = await core.listWUs({ projectRoot });
|
|
540
|
+
const completedWuIds = await getCompletedWuIdsFromStamps(core, projectRoot);
|
|
541
|
+
const declaredWuIds = extractInitiativeWuIds(initiativeDoc.wus);
|
|
542
|
+
const declaredWuIdSet = new Set(declaredWuIds);
|
|
543
|
+
const normalizedInitiativeRefs = new Set([
|
|
544
|
+
normalizeToken(parsedInput.data.initiative),
|
|
545
|
+
normalizeToken(initiativeId),
|
|
546
|
+
normalizeToken(initiativeSlug),
|
|
547
|
+
]);
|
|
548
|
+
const wuById = new Map(allWUs.map((wu) => [wu.id, wu]));
|
|
549
|
+
const inferredWuIds = allWUs
|
|
550
|
+
.filter((wu) => normalizedInitiativeRefs.has(normalizeToken(wu.initiative)))
|
|
551
|
+
.map((wu) => wu.id);
|
|
552
|
+
const orderedWuIds = declaredWuIds.length > 0 ? declaredWuIds : inferredWuIds;
|
|
553
|
+
const dedupedWuIds = [...new Set(orderedWuIds)];
|
|
554
|
+
const statusEntries = dedupedWuIds.map((wuId) => {
|
|
555
|
+
const wu = wuById.get(wuId);
|
|
556
|
+
const fallbackStatus = declaredWuIdSet.has(wuId) ? STATUS_READY : STATUS_UNKNOWN;
|
|
557
|
+
return {
|
|
558
|
+
id: wuId,
|
|
559
|
+
title: wu?.title ?? wuId,
|
|
560
|
+
lane: wu?.lane ?? '',
|
|
561
|
+
status: completedWuIds.has(wuId) ? STATUS_DONE : (wu?.status ?? fallbackStatus),
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
const progress = countProgress(statusEntries);
|
|
565
|
+
const config = core.getConfig({ projectRoot });
|
|
566
|
+
const laneConfigMap = resolveLanePolicyConfig(config);
|
|
567
|
+
const laneAvailability = computeLaneAvailability(statusEntries, laneConfigMap);
|
|
568
|
+
const lifecycleStatus = deriveInitiativeLifecycleStatus(initiativeDoc.status, initiativeDoc.phases);
|
|
569
|
+
const rawStatus = normalizeLifecycleStatus(initiativeDoc.status);
|
|
570
|
+
const message = formatInitiativeStatusMessage({
|
|
571
|
+
initiativeId,
|
|
572
|
+
initiativeTitle: initiativeDoc.title ?? initiativeId,
|
|
573
|
+
lifecycleStatus,
|
|
574
|
+
rawStatus,
|
|
575
|
+
progress,
|
|
576
|
+
wus: statusEntries,
|
|
577
|
+
laneAvailability,
|
|
578
|
+
});
|
|
579
|
+
return createSuccessOutput({
|
|
580
|
+
message,
|
|
581
|
+
initiative: {
|
|
582
|
+
id: initiativeId,
|
|
583
|
+
slug: initiativeSlug || undefined,
|
|
584
|
+
title: initiativeDoc.title ?? initiativeId,
|
|
585
|
+
lifecycle_status: lifecycleStatus,
|
|
586
|
+
raw_status: rawStatus || undefined,
|
|
587
|
+
},
|
|
588
|
+
progress,
|
|
589
|
+
wus: statusEntries,
|
|
590
|
+
lane_availability: laneAvailability,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
catch (cause) {
|
|
594
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.ORCHESTRATE_INIT_STATUS_ERROR, cause.message);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const orchestrateMonitorInProcess = async (rawInput, context) => {
|
|
598
|
+
const parsedInput = ORCHESTRATE_MONITOR_INPUT_SCHEMA.safeParse(rawInput);
|
|
599
|
+
if (!parsedInput.success) {
|
|
600
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const core = await getCoreLazy();
|
|
604
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
605
|
+
if (parsedInput.data.signals_only) {
|
|
606
|
+
const sinceInput = parsedInput.data.since && parsedInput.data.since.trim().length > 0
|
|
607
|
+
? parsedInput.data.since
|
|
608
|
+
: ORCHESTRATE_MONITOR_DEFAULT_SINCE;
|
|
609
|
+
const sinceDate = parseSinceInputToDate(sinceInput);
|
|
610
|
+
const allSignals = await loadRecentSignals(core, projectRoot, sinceDate);
|
|
611
|
+
const filteredSignals = parsedInput.data.wu
|
|
612
|
+
? allSignals.filter((signal) => signal.wuId === parsedInput.data.wu)
|
|
613
|
+
: allSignals;
|
|
614
|
+
const lines = [
|
|
615
|
+
`Signals since ${sinceDate.toISOString()}:`,
|
|
616
|
+
`Count: ${filteredSignals.length}`,
|
|
617
|
+
];
|
|
618
|
+
if (filteredSignals.length > 0) {
|
|
619
|
+
for (const signal of filteredSignals) {
|
|
620
|
+
lines.push(`${signal.timestamp} [${signal.wuId ?? 'system'}] ${signal.type}: ${signal.message ?? ''}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
lines.push('No signals found.');
|
|
625
|
+
}
|
|
626
|
+
return createSuccessOutput({
|
|
627
|
+
message: lines.join('\n'),
|
|
628
|
+
since: sinceInput,
|
|
629
|
+
signals: filteredSignals,
|
|
630
|
+
total: filteredSignals.length,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const thresholdMinutes = parsedInput.data.threshold ?? core.DEFAULT_THRESHOLD_MINUTES;
|
|
634
|
+
const stateDir = path.join(projectRoot, core.LUMENFLOW_PATHS.STATE_DIR);
|
|
635
|
+
const registryStore = new core.DelegationRegistryStore(stateDir);
|
|
636
|
+
let delegations = [];
|
|
637
|
+
try {
|
|
638
|
+
await registryStore.load();
|
|
639
|
+
delegations = registryStore.getAllDelegations();
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
delegations = [];
|
|
643
|
+
}
|
|
644
|
+
const analysis = core.analyzeDelegations(delegations);
|
|
645
|
+
const stuckDelegations = core.detectStuckDelegations(delegations, thresholdMinutes);
|
|
646
|
+
const zombieLocks = await core.checkZombieLocks({ baseDir: projectRoot });
|
|
647
|
+
const suggestions = core.generateSuggestions(stuckDelegations, zombieLocks);
|
|
648
|
+
const monitorResult = {
|
|
649
|
+
analysis,
|
|
650
|
+
stuckDelegations,
|
|
651
|
+
zombieLocks,
|
|
652
|
+
suggestions,
|
|
653
|
+
dryRun: parsedInput.data.dry_run ?? false,
|
|
654
|
+
};
|
|
655
|
+
let recoveryResults;
|
|
656
|
+
if (parsedInput.data.recover) {
|
|
657
|
+
recoveryResults = await core.runRecovery(stuckDelegations, {
|
|
658
|
+
baseDir: projectRoot,
|
|
659
|
+
dryRun: parsedInput.data.dry_run ?? false,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
let monitorOutput = core.formatMonitorOutput(monitorResult);
|
|
663
|
+
if (recoveryResults && recoveryResults.length > 0) {
|
|
664
|
+
monitorOutput = `${monitorOutput}\n\n${core.formatRecoveryResults(recoveryResults)}`;
|
|
665
|
+
}
|
|
666
|
+
if (stuckDelegations.length > 0 || zombieLocks.length > 0) {
|
|
667
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.ORCHESTRATE_MONITOR_ERROR, monitorOutput);
|
|
668
|
+
}
|
|
669
|
+
return createSuccessOutput({
|
|
670
|
+
message: monitorOutput,
|
|
671
|
+
...monitorResult,
|
|
672
|
+
recoveryResults,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
catch (cause) {
|
|
676
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.ORCHESTRATE_MONITOR_ERROR, cause.message);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const delegationListInProcess = async (rawInput, context) => {
|
|
680
|
+
const parsedInput = DELEGATION_LIST_INPUT_SCHEMA.safeParse(rawInput);
|
|
681
|
+
if (!parsedInput.success) {
|
|
682
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
683
|
+
}
|
|
684
|
+
if (!parsedInput.data.wu && !parsedInput.data.initiative) {
|
|
685
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.INVALID_INPUT, 'Either wu or initiative is required');
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
const core = await getCoreLazy();
|
|
689
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
690
|
+
const config = core.getConfig({ projectRoot });
|
|
691
|
+
const registryDir = path.join(projectRoot, config.state.stateDir);
|
|
692
|
+
const wuDir = path.join(projectRoot, config.directories.wuDir);
|
|
693
|
+
if (parsedInput.data.wu) {
|
|
694
|
+
const wuId = parsedInput.data.wu.toUpperCase();
|
|
695
|
+
if (!WU_ID_PATTERN.test(wuId)) {
|
|
696
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.INVALID_INPUT, `Invalid WU ID format: ${parsedInput.data.wu}. Expected format: WU-XXX`);
|
|
697
|
+
}
|
|
698
|
+
const delegations = await core.getDelegationsByWU(wuId, registryDir);
|
|
699
|
+
if (parsedInput.data.json) {
|
|
700
|
+
const tree = core.buildDelegationTree(delegations, wuId);
|
|
701
|
+
return createSuccessOutput(core.treeToJSON(tree));
|
|
702
|
+
}
|
|
703
|
+
if (delegations.length === 0) {
|
|
704
|
+
return createSuccessOutput({
|
|
705
|
+
message: `${DELEGATION_LIST_LOG_PREFIX} No delegations found for ${wuId}`,
|
|
706
|
+
delegations,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const tree = core.buildDelegationTree(delegations, wuId);
|
|
710
|
+
return createSuccessOutput({
|
|
711
|
+
message: `${DELEGATION_LIST_LOG_PREFIX} Delegation tree for ${wuId}:\n\n${core.formatDelegationTree(tree)}\n\nTotal: ${delegations.length} delegation(s)`,
|
|
712
|
+
delegations,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const initiativeId = parsedInput.data.initiative.toUpperCase();
|
|
716
|
+
const delegations = await core.getDelegationsByInitiative(initiativeId, registryDir, wuDir);
|
|
717
|
+
if (parsedInput.data.json) {
|
|
718
|
+
return createSuccessOutput(delegations);
|
|
719
|
+
}
|
|
720
|
+
if (delegations.length === 0) {
|
|
721
|
+
return createSuccessOutput({
|
|
722
|
+
message: `${DELEGATION_LIST_LOG_PREFIX} No delegations found for ${initiativeId}`,
|
|
723
|
+
delegations,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
const typedDelegations = delegations;
|
|
727
|
+
const targetWuIds = new Set(typedDelegations
|
|
728
|
+
.map((record) => record.targetWuId)
|
|
729
|
+
.filter((wuId) => typeof wuId === 'string'));
|
|
730
|
+
const rootWuIds = [
|
|
731
|
+
...new Set(typedDelegations
|
|
732
|
+
.map((record) => record.parentWuId)
|
|
733
|
+
.filter((wuId) => typeof wuId === 'string')),
|
|
734
|
+
].filter((wuId) => !targetWuIds.has(wuId));
|
|
735
|
+
const lines = [`${DELEGATION_LIST_LOG_PREFIX} Delegations for ${initiativeId}:`, ''];
|
|
736
|
+
for (const rootWuId of rootWuIds) {
|
|
737
|
+
const tree = core.buildDelegationTree(delegations, rootWuId);
|
|
738
|
+
lines.push(core.formatDelegationTree(tree));
|
|
739
|
+
lines.push('');
|
|
740
|
+
}
|
|
741
|
+
lines.push(`Total: ${delegations.length} delegation(s) across ${rootWuIds.length} root WU(s)`);
|
|
742
|
+
return createSuccessOutput({
|
|
743
|
+
message: lines.join('\n'),
|
|
744
|
+
delegations,
|
|
745
|
+
root_wu_ids: rootWuIds,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
catch (cause) {
|
|
749
|
+
return createFailureOutput(ORCHESTRATION_TOOL_ERROR_CODES.DELEGATION_LIST_ERROR, cause.message);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
function createFailureOutput(code, message) {
|
|
753
|
+
return {
|
|
754
|
+
success: false,
|
|
755
|
+
error: {
|
|
756
|
+
code,
|
|
757
|
+
message,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function createSuccessOutput(data) {
|
|
762
|
+
return {
|
|
763
|
+
success: true,
|
|
764
|
+
data,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function resolveWorkspaceRoot(context) {
|
|
768
|
+
const root = context.metadata?.[RUNTIME_PROJECT_ROOT_METADATA_KEY];
|
|
769
|
+
if (typeof root === 'string' && root.trim().length > 0) {
|
|
770
|
+
return path.resolve(root);
|
|
771
|
+
}
|
|
772
|
+
return process.cwd();
|
|
773
|
+
}
|
|
774
|
+
function resolveTargetPath(context, inputPath) {
|
|
775
|
+
return path.resolve(resolveWorkspaceRoot(context), inputPath);
|
|
776
|
+
}
|
|
777
|
+
function resolveEncoding(encoding) {
|
|
778
|
+
return (encoding ?? UTF8_ENCODING);
|
|
779
|
+
}
|
|
780
|
+
function extractLineRange(content, startLine, endLine) {
|
|
781
|
+
if (startLine === undefined && endLine === undefined) {
|
|
782
|
+
return content;
|
|
783
|
+
}
|
|
784
|
+
const lines = content.split('\n');
|
|
785
|
+
const start = (startLine ?? 1) - 1;
|
|
786
|
+
const end = endLine ?? lines.length;
|
|
787
|
+
return lines.slice(start, end).join('\n');
|
|
788
|
+
}
|
|
789
|
+
function countOccurrences(content, searchText) {
|
|
790
|
+
if (!searchText) {
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
let count = 0;
|
|
794
|
+
let cursor = 0;
|
|
795
|
+
while (cursor < content.length) {
|
|
796
|
+
const index = content.indexOf(searchText, cursor);
|
|
797
|
+
if (index === -1) {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
count += 1;
|
|
801
|
+
cursor = index + searchText.length;
|
|
802
|
+
}
|
|
803
|
+
return count;
|
|
804
|
+
}
|
|
805
|
+
async function getPathInfo(targetPath) {
|
|
806
|
+
try {
|
|
807
|
+
const targetStats = await stat(targetPath);
|
|
808
|
+
return { exists: true, isDirectory: targetStats.isDirectory() };
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return { exists: false, isDirectory: false };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function countItemsInDirectory(directoryPath) {
|
|
815
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
816
|
+
let count = 0;
|
|
817
|
+
for (const entry of entries) {
|
|
818
|
+
count += 1;
|
|
819
|
+
if (entry.isDirectory()) {
|
|
820
|
+
count += await countItemsInDirectory(path.join(directoryPath, entry.name));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return count;
|
|
824
|
+
}
|
|
825
|
+
const fileReadInProcess = async (rawInput, context) => {
|
|
826
|
+
const parsedInput = FILE_READ_INPUT_SCHEMA.safeParse(rawInput);
|
|
827
|
+
if (!parsedInput.success) {
|
|
828
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
829
|
+
}
|
|
830
|
+
const targetPath = resolveTargetPath(context, parsedInput.data.path);
|
|
831
|
+
const encoding = resolveEncoding(parsedInput.data.encoding);
|
|
832
|
+
const maxSize = parsedInput.data.max_size ?? DEFAULT_FILE_READ_MAX_SIZE_BYTES;
|
|
833
|
+
try {
|
|
834
|
+
const targetStats = await stat(targetPath);
|
|
835
|
+
if (targetStats.size > maxSize) {
|
|
836
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_READ_TOO_LARGE, `File size (${targetStats.size} bytes) exceeds maximum allowed (${maxSize} bytes).`);
|
|
837
|
+
}
|
|
838
|
+
const content = await readFile(targetPath, { encoding });
|
|
839
|
+
const selectedContent = extractLineRange(content, parsedInput.data.start_line, parsedInput.data.end_line);
|
|
840
|
+
const totalLineCount = content.length === 0 ? 0 : content.split('\n').length;
|
|
841
|
+
return createSuccessOutput({
|
|
842
|
+
content: selectedContent,
|
|
843
|
+
metadata: {
|
|
844
|
+
size_bytes: targetStats.size,
|
|
845
|
+
line_count: totalLineCount,
|
|
846
|
+
lines_returned: selectedContent.length === 0 ? 0 : selectedContent.split('\n').length,
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
catch (cause) {
|
|
851
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_READ_FAILED, cause.message);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
const fileWriteInProcess = async (rawInput, context) => {
|
|
855
|
+
const parsedInput = FILE_WRITE_INPUT_SCHEMA.safeParse(rawInput);
|
|
856
|
+
if (!parsedInput.success) {
|
|
857
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
858
|
+
}
|
|
859
|
+
const targetPath = resolveTargetPath(context, parsedInput.data.path);
|
|
860
|
+
const encoding = resolveEncoding(parsedInput.data.encoding);
|
|
861
|
+
const createDirectories = !parsedInput.data.no_create_dirs;
|
|
862
|
+
const parentDirectory = path.dirname(targetPath);
|
|
863
|
+
try {
|
|
864
|
+
if (createDirectories) {
|
|
865
|
+
await mkdir(parentDirectory, { recursive: true });
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
const parentInfo = await getPathInfo(parentDirectory);
|
|
869
|
+
if (!parentInfo.exists || !parentInfo.isDirectory) {
|
|
870
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_WRITE_FAILED, `${FILE_TOOL_MESSAGES.PARENT_DIRECTORY_MISSING}: ${parentDirectory}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
await writeFile(targetPath, parsedInput.data.content, { encoding });
|
|
874
|
+
return createSuccessOutput({
|
|
875
|
+
message: FILE_TOOL_MESSAGES.FILE_WRITTEN,
|
|
876
|
+
path: targetPath,
|
|
877
|
+
bytes_written: Buffer.byteLength(parsedInput.data.content, encoding),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
catch (cause) {
|
|
881
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_WRITE_FAILED, cause.message);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const fileEditInProcess = async (rawInput, context) => {
|
|
885
|
+
const parsedInput = FILE_EDIT_INPUT_SCHEMA.safeParse(rawInput);
|
|
886
|
+
if (!parsedInput.success) {
|
|
887
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
888
|
+
}
|
|
889
|
+
const targetPath = resolveTargetPath(context, parsedInput.data.path);
|
|
890
|
+
const encoding = resolveEncoding(parsedInput.data.encoding);
|
|
891
|
+
const replaceAll = parsedInput.data.replace_all ?? false;
|
|
892
|
+
try {
|
|
893
|
+
const content = await readFile(targetPath, { encoding });
|
|
894
|
+
const occurrenceCount = countOccurrences(content, parsedInput.data.old_string);
|
|
895
|
+
if (occurrenceCount === 0) {
|
|
896
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_EDIT_TARGET_NOT_FOUND, `old_string not found in file: ${parsedInput.data.old_string}`);
|
|
897
|
+
}
|
|
898
|
+
if (occurrenceCount > 1 && !replaceAll) {
|
|
899
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_EDIT_NOT_UNIQUE, `old_string is not unique in file (found ${occurrenceCount} occurrences).`);
|
|
900
|
+
}
|
|
901
|
+
const nextContent = replaceAll
|
|
902
|
+
? content.split(parsedInput.data.old_string).join(parsedInput.data.new_string)
|
|
903
|
+
: content.replace(parsedInput.data.old_string, parsedInput.data.new_string);
|
|
904
|
+
await writeFile(targetPath, nextContent, { encoding });
|
|
905
|
+
return createSuccessOutput({
|
|
906
|
+
message: FILE_TOOL_MESSAGES.FILE_EDITED,
|
|
907
|
+
path: targetPath,
|
|
908
|
+
replacements: replaceAll ? occurrenceCount : 1,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
catch (cause) {
|
|
912
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_EDIT_FAILED, cause.message);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
const fileDeleteInProcess = async (rawInput, context) => {
|
|
916
|
+
const parsedInput = FILE_DELETE_INPUT_SCHEMA.safeParse(rawInput);
|
|
917
|
+
if (!parsedInput.success) {
|
|
918
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
919
|
+
}
|
|
920
|
+
const targetPath = resolveTargetPath(context, parsedInput.data.path);
|
|
921
|
+
const recursive = parsedInput.data.recursive ?? false;
|
|
922
|
+
const force = parsedInput.data.force ?? false;
|
|
923
|
+
try {
|
|
924
|
+
const targetInfo = await getPathInfo(targetPath);
|
|
925
|
+
if (!targetInfo.exists) {
|
|
926
|
+
if (force) {
|
|
927
|
+
return createSuccessOutput({
|
|
928
|
+
message: FILE_TOOL_MESSAGES.DELETE_COMPLETE,
|
|
929
|
+
metadata: {
|
|
930
|
+
deleted_count: 0,
|
|
931
|
+
was_directory: false,
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_DELETE_FAILED, `${FILE_TOOL_MESSAGES.PATH_NOT_FOUND}: ${targetPath}`);
|
|
936
|
+
}
|
|
937
|
+
if (targetInfo.isDirectory && !recursive) {
|
|
938
|
+
const entries = await readdir(targetPath);
|
|
939
|
+
if (entries.length > 0) {
|
|
940
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_DELETE_FAILED, FILE_TOOL_MESSAGES.DIRECTORY_NOT_EMPTY);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
let deletedCount = 1;
|
|
944
|
+
if (targetInfo.isDirectory && recursive) {
|
|
945
|
+
deletedCount += await countItemsInDirectory(targetPath);
|
|
946
|
+
}
|
|
947
|
+
await rm(targetPath, { recursive, force });
|
|
948
|
+
return createSuccessOutput({
|
|
949
|
+
message: FILE_TOOL_MESSAGES.DELETE_COMPLETE,
|
|
950
|
+
metadata: {
|
|
951
|
+
deleted_count: deletedCount,
|
|
952
|
+
was_directory: targetInfo.isDirectory,
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
catch (cause) {
|
|
957
|
+
return createFailureOutput(FILE_TOOL_ERROR_CODES.FILE_DELETE_FAILED, cause.message);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
function resolveCommandBaseDir(context, baseDir) {
|
|
961
|
+
if (!baseDir || baseDir.trim().length === 0) {
|
|
962
|
+
return resolveWorkspaceRoot(context);
|
|
963
|
+
}
|
|
964
|
+
return path.resolve(resolveWorkspaceRoot(context), baseDir);
|
|
965
|
+
}
|
|
966
|
+
function normalizeDryRun(execute, dryRun) {
|
|
967
|
+
if (dryRun !== undefined) {
|
|
968
|
+
return dryRun;
|
|
969
|
+
}
|
|
970
|
+
return !execute;
|
|
971
|
+
}
|
|
972
|
+
function toIsoTimestamp(timestamp, fallback) {
|
|
973
|
+
const candidate = timestamp ?? fallback;
|
|
974
|
+
if (!candidate) {
|
|
975
|
+
return new Date().toISOString();
|
|
976
|
+
}
|
|
977
|
+
if (candidate.includes('T')) {
|
|
978
|
+
return candidate;
|
|
979
|
+
}
|
|
980
|
+
const parsed = new Date(candidate);
|
|
981
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
982
|
+
return new Date().toISOString();
|
|
983
|
+
}
|
|
984
|
+
return parsed.toISOString();
|
|
985
|
+
}
|
|
986
|
+
function calculateDaysSince(dateString) {
|
|
987
|
+
if (!dateString) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
const parsedDate = new Date(dateString);
|
|
991
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
return Math.floor((Date.now() - parsedDate.getTime()) / STATE_RUNTIME_CONSTANTS.ONE_DAY_MS);
|
|
995
|
+
}
|
|
996
|
+
async function loadWuLifecycleDocuments(core, wuDir) {
|
|
997
|
+
const directoryInfo = await getPathInfo(wuDir);
|
|
998
|
+
if (!directoryInfo.exists || !directoryInfo.isDirectory) {
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
const files = await readdir(wuDir);
|
|
1002
|
+
const documents = [];
|
|
1003
|
+
for (const fileName of files) {
|
|
1004
|
+
if (!fileName.startsWith(STATE_RUNTIME_CONSTANTS.WU_FILE_PREFIX) ||
|
|
1005
|
+
!fileName.endsWith(STATE_RUNTIME_CONSTANTS.YAML_EXTENSION)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const filePath = path.join(wuDir, fileName);
|
|
1009
|
+
try {
|
|
1010
|
+
const rawDoc = core.readWURaw(filePath);
|
|
1011
|
+
if (!rawDoc || typeof rawDoc.id !== 'string' || typeof rawDoc.status !== 'string') {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
documents.push({
|
|
1015
|
+
id: rawDoc.id,
|
|
1016
|
+
status: rawDoc.status,
|
|
1017
|
+
title: typeof rawDoc.title === 'string' ? rawDoc.title : undefined,
|
|
1018
|
+
lane: typeof rawDoc.lane === 'string' ? rawDoc.lane : undefined,
|
|
1019
|
+
created: typeof rawDoc.created === 'string' ? rawDoc.created : undefined,
|
|
1020
|
+
claimed_at: typeof rawDoc.claimed_at === 'string' ? rawDoc.claimed_at : undefined,
|
|
1021
|
+
completed: typeof rawDoc.completed === 'string' ? rawDoc.completed : undefined,
|
|
1022
|
+
completed_at: typeof rawDoc.completed_at === 'string' ? rawDoc.completed_at : undefined,
|
|
1023
|
+
updated: typeof rawDoc.updated === 'string' ? rawDoc.updated : undefined,
|
|
1024
|
+
filePath,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return documents;
|
|
1032
|
+
}
|
|
1033
|
+
function inferBootstrapEvents(core, wu) {
|
|
1034
|
+
const readyLikeStatuses = new Set([
|
|
1035
|
+
core.WU_STATUS.READY,
|
|
1036
|
+
core.WU_STATUS.BACKLOG,
|
|
1037
|
+
core.WU_STATUS.TODO,
|
|
1038
|
+
]);
|
|
1039
|
+
const doneStatuses = new Set([core.WU_STATUS.DONE, core.WU_STATUS.COMPLETED]);
|
|
1040
|
+
const normalizedStatus = wu.status;
|
|
1041
|
+
if (readyLikeStatuses.has(normalizedStatus)) {
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
const claimTimestamp = toIsoTimestamp(wu.claimed_at, wu.created);
|
|
1045
|
+
const events = [
|
|
1046
|
+
{
|
|
1047
|
+
type: STATE_RUNTIME_EVENT_TYPES.CLAIM,
|
|
1048
|
+
wuId: wu.id,
|
|
1049
|
+
lane: wu.lane ?? STATE_RUNTIME_CONSTANTS.UNKNOWN_LANE,
|
|
1050
|
+
title: wu.title ?? STATE_RUNTIME_CONSTANTS.UNTITLED_WU,
|
|
1051
|
+
timestamp: claimTimestamp,
|
|
1052
|
+
},
|
|
1053
|
+
];
|
|
1054
|
+
if (normalizedStatus === core.WU_STATUS.BLOCKED) {
|
|
1055
|
+
const blockedAt = new Date(claimTimestamp);
|
|
1056
|
+
blockedAt.setSeconds(blockedAt.getSeconds() + 1);
|
|
1057
|
+
events.push({
|
|
1058
|
+
type: STATE_RUNTIME_EVENT_TYPES.BLOCK,
|
|
1059
|
+
wuId: wu.id,
|
|
1060
|
+
timestamp: blockedAt.toISOString(),
|
|
1061
|
+
reason: STATE_RUNTIME_CONSTANTS.BOOTSTRAP_BLOCK_REASON,
|
|
1062
|
+
});
|
|
1063
|
+
return events;
|
|
1064
|
+
}
|
|
1065
|
+
if (doneStatuses.has(normalizedStatus)) {
|
|
1066
|
+
events.push({
|
|
1067
|
+
type: STATE_RUNTIME_EVENT_TYPES.COMPLETE,
|
|
1068
|
+
wuId: wu.id,
|
|
1069
|
+
timestamp: toIsoTimestamp(wu.completed_at ?? wu.completed, claimTimestamp),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
return events;
|
|
1073
|
+
}
|
|
1074
|
+
async function listActiveWuIds(projectRoot) {
|
|
1075
|
+
const core = await getCoreLazy();
|
|
1076
|
+
const activeStatuses = new Set([core.WU_STATUS.IN_PROGRESS, core.WU_STATUS.BLOCKED]);
|
|
1077
|
+
const wus = await core.listWUs({ projectRoot });
|
|
1078
|
+
return new Set(wus.filter((wu) => activeStatuses.has(wu.status)).map((wu) => wu.id));
|
|
1079
|
+
}
|
|
1080
|
+
async function readNdjsonRecords(filePath) {
|
|
1081
|
+
try {
|
|
1082
|
+
const content = await readFile(filePath, UTF8_ENCODING);
|
|
1083
|
+
return content
|
|
1084
|
+
.split('\n')
|
|
1085
|
+
.map((line) => line.trim())
|
|
1086
|
+
.filter((line) => line.length > 0)
|
|
1087
|
+
.map((line) => {
|
|
1088
|
+
try {
|
|
1089
|
+
return JSON.parse(line);
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
})
|
|
1095
|
+
.filter((line) => line !== null);
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return [];
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function buildStateDoctorDeps(core, projectRoot) {
|
|
1102
|
+
const config = core.getConfig({ projectRoot });
|
|
1103
|
+
const wuDir = path.join(projectRoot, config.directories.wuDir);
|
|
1104
|
+
const stampsDir = path.join(projectRoot, config.state.stampsDir);
|
|
1105
|
+
const stateDir = path.join(projectRoot, config.state.stateDir);
|
|
1106
|
+
const signalsPath = path.join(projectRoot, core.LUMENFLOW_PATHS.MEMORY_SIGNALS);
|
|
1107
|
+
const eventsPath = path.join(projectRoot, core.LUMENFLOW_PATHS.WU_EVENTS);
|
|
1108
|
+
return {
|
|
1109
|
+
listWUs: async () => {
|
|
1110
|
+
const documents = await loadWuLifecycleDocuments(core, wuDir);
|
|
1111
|
+
return documents.map((document) => ({
|
|
1112
|
+
id: document.id,
|
|
1113
|
+
status: document.status,
|
|
1114
|
+
lane: document.lane,
|
|
1115
|
+
title: document.title,
|
|
1116
|
+
}));
|
|
1117
|
+
},
|
|
1118
|
+
listStamps: async () => {
|
|
1119
|
+
try {
|
|
1120
|
+
const files = await readdir(stampsDir);
|
|
1121
|
+
return files
|
|
1122
|
+
.filter((file) => file.endsWith(STATE_RUNTIME_CONSTANTS.DONE_STAMP_EXTENSION))
|
|
1123
|
+
.map((file) => file.slice(0, -1 * STATE_RUNTIME_CONSTANTS.DONE_STAMP_EXTENSION.length));
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
listSignals: async () => {
|
|
1130
|
+
const signals = await readNdjsonRecords(signalsPath);
|
|
1131
|
+
return signals
|
|
1132
|
+
.filter((signal) => typeof signal.id === 'string')
|
|
1133
|
+
.map((signal) => ({
|
|
1134
|
+
id: String(signal.id),
|
|
1135
|
+
wuId: typeof signal.wuId === 'string'
|
|
1136
|
+
? signal.wuId
|
|
1137
|
+
: typeof signal.wu_id === 'string'
|
|
1138
|
+
? signal.wu_id
|
|
1139
|
+
: undefined,
|
|
1140
|
+
timestamp: typeof signal.timestamp === 'string' ? signal.timestamp : undefined,
|
|
1141
|
+
message: typeof signal.message === 'string' ? signal.message : undefined,
|
|
1142
|
+
}));
|
|
1143
|
+
},
|
|
1144
|
+
listEvents: async () => {
|
|
1145
|
+
const events = await readNdjsonRecords(eventsPath);
|
|
1146
|
+
return events
|
|
1147
|
+
.filter((event) => (typeof event.wuId === 'string' || typeof event.wu_id === 'string') &&
|
|
1148
|
+
typeof event.type === 'string')
|
|
1149
|
+
.map((event) => ({
|
|
1150
|
+
wuId: typeof event.wuId === 'string'
|
|
1151
|
+
? event.wuId
|
|
1152
|
+
: typeof event.wu_id === 'string'
|
|
1153
|
+
? event.wu_id
|
|
1154
|
+
: '',
|
|
1155
|
+
type: String(event.type),
|
|
1156
|
+
timestamp: typeof event.timestamp === 'string' ? event.timestamp : undefined,
|
|
1157
|
+
}));
|
|
1158
|
+
},
|
|
1159
|
+
removeSignal: async (signalId) => {
|
|
1160
|
+
const signals = await readNdjsonRecords(signalsPath);
|
|
1161
|
+
const retainedSignals = signals.filter((signal) => signal.id !== signalId);
|
|
1162
|
+
const payload = retainedSignals.map((signal) => JSON.stringify(signal)).join('\n') +
|
|
1163
|
+
(retainedSignals.length > 0 ? '\n' : '');
|
|
1164
|
+
await writeFile(signalsPath, payload, UTF8_ENCODING);
|
|
1165
|
+
},
|
|
1166
|
+
removeEvent: async (wuId) => {
|
|
1167
|
+
const events = await readNdjsonRecords(eventsPath);
|
|
1168
|
+
const retainedEvents = events.filter((event) => event.wuId !== wuId && event.wu_id !== wuId);
|
|
1169
|
+
const payload = retainedEvents.map((event) => JSON.stringify(event)).join('\n') +
|
|
1170
|
+
(retainedEvents.length > 0 ? '\n' : '');
|
|
1171
|
+
await writeFile(eventsPath, payload, UTF8_ENCODING);
|
|
1172
|
+
},
|
|
1173
|
+
createStamp: async (wuId, title) => {
|
|
1174
|
+
await mkdir(stampsDir, { recursive: true });
|
|
1175
|
+
const stampPath = path.join(stampsDir, `${wuId}${STATE_RUNTIME_CONSTANTS.DONE_STAMP_EXTENSION}`);
|
|
1176
|
+
const completedDate = new Date().toISOString().slice(0, 10);
|
|
1177
|
+
const stampContent = `WU ${wuId} — ${title}\nCompleted: ${completedDate}\n`;
|
|
1178
|
+
await writeFile(stampPath, stampContent, UTF8_ENCODING);
|
|
1179
|
+
},
|
|
1180
|
+
emitEvent: async (event) => {
|
|
1181
|
+
const stateStore = new core.WUStateStore(stateDir);
|
|
1182
|
+
await stateStore.load();
|
|
1183
|
+
if (event.type === STATE_RUNTIME_EVENT_TYPES.RELEASE) {
|
|
1184
|
+
await stateStore.release(event.wuId, event.reason ?? STATE_RUNTIME_CONSTANTS.STATE_DOCTOR_FIX_REASON);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
await stateStore.complete(event.wuId);
|
|
1188
|
+
},
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const backlogPruneInProcess = async (rawInput, context) => {
|
|
1192
|
+
const parsedInput = BACKLOG_PRUNE_INPUT_SCHEMA.safeParse(rawInput);
|
|
1193
|
+
if (!parsedInput.success) {
|
|
1194
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const core = await getCoreLazy();
|
|
1198
|
+
const workspaceRoot = resolveWorkspaceRoot(context);
|
|
1199
|
+
const config = core.getConfig({ projectRoot: workspaceRoot });
|
|
1200
|
+
const wuDir = path.join(workspaceRoot, config.directories.wuDir);
|
|
1201
|
+
const wuDocuments = await loadWuLifecycleDocuments(core, wuDir);
|
|
1202
|
+
const dryRun = normalizeDryRun(parsedInput.data.execute, parsedInput.data.dry_run);
|
|
1203
|
+
const staleDaysInProgress = parsedInput.data.stale_days_in_progress ??
|
|
1204
|
+
STATE_RUNTIME_CONSTANTS.DEFAULT_STALE_DAYS_IN_PROGRESS;
|
|
1205
|
+
const staleDaysReady = parsedInput.data.stale_days_ready ?? STATE_RUNTIME_CONSTANTS.DEFAULT_STALE_DAYS_READY;
|
|
1206
|
+
const archiveDays = parsedInput.data.archive_days ?? STATE_RUNTIME_CONSTANTS.DEFAULT_ARCHIVE_DAYS;
|
|
1207
|
+
const doneStatuses = new Set([core.WU_STATUS.DONE, core.WU_STATUS.COMPLETED]);
|
|
1208
|
+
const staleCandidates = [];
|
|
1209
|
+
const archivableCandidates = [];
|
|
1210
|
+
const healthyCandidates = [];
|
|
1211
|
+
for (const wu of wuDocuments) {
|
|
1212
|
+
if (doneStatuses.has(wu.status)) {
|
|
1213
|
+
const completedAge = calculateDaysSince(wu.completed ?? wu.completed_at);
|
|
1214
|
+
if (completedAge !== null && completedAge > archiveDays) {
|
|
1215
|
+
archivableCandidates.push(wu);
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
healthyCandidates.push(wu);
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (wu.status === core.WU_STATUS.BLOCKED) {
|
|
1223
|
+
healthyCandidates.push(wu);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const lastActivity = wu.updated ?? wu.created;
|
|
1227
|
+
const daysSinceActivity = calculateDaysSince(lastActivity);
|
|
1228
|
+
if (daysSinceActivity === null) {
|
|
1229
|
+
healthyCandidates.push(wu);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const isInProgressStale = wu.status === core.WU_STATUS.IN_PROGRESS && daysSinceActivity > staleDaysInProgress;
|
|
1233
|
+
const isReadyLikeStale = (wu.status === core.WU_STATUS.READY ||
|
|
1234
|
+
wu.status === core.WU_STATUS.BACKLOG ||
|
|
1235
|
+
wu.status === core.WU_STATUS.TODO) &&
|
|
1236
|
+
daysSinceActivity > staleDaysReady;
|
|
1237
|
+
if (isInProgressStale || isReadyLikeStale) {
|
|
1238
|
+
staleCandidates.push(wu);
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
healthyCandidates.push(wu);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
let taggedCount = 0;
|
|
1245
|
+
if (!dryRun) {
|
|
1246
|
+
const noteDate = new Date().toISOString().slice(0, 10);
|
|
1247
|
+
for (const staleWu of staleCandidates) {
|
|
1248
|
+
try {
|
|
1249
|
+
const wuDoc = core.readWURaw(staleWu.filePath);
|
|
1250
|
+
core.appendNote(wuDoc, `[${noteDate}] ${STATE_RUNTIME_CONSTANTS.STALE_NOTE_TEMPLATE}`);
|
|
1251
|
+
core.writeWU(staleWu.filePath, wuDoc);
|
|
1252
|
+
taggedCount += 1;
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return createSuccessOutput({
|
|
1260
|
+
dry_run: dryRun,
|
|
1261
|
+
total_wus: wuDocuments.length,
|
|
1262
|
+
stale_count: staleCandidates.length,
|
|
1263
|
+
stale_ids: staleCandidates.map((wu) => wu.id),
|
|
1264
|
+
archivable_count: archivableCandidates.length,
|
|
1265
|
+
archivable_ids: archivableCandidates.map((wu) => wu.id),
|
|
1266
|
+
healthy_count: healthyCandidates.length,
|
|
1267
|
+
tagged_count: taggedCount,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
catch (cause) {
|
|
1271
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.BACKLOG_PRUNE_FAILED, cause.message);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
const stateBootstrapInProcess = async (rawInput, context) => {
|
|
1275
|
+
const parsedInput = STATE_BOOTSTRAP_INPUT_SCHEMA.safeParse(rawInput);
|
|
1276
|
+
if (!parsedInput.success) {
|
|
1277
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const core = await getCoreLazy();
|
|
1281
|
+
const workspaceRoot = resolveWorkspaceRoot(context);
|
|
1282
|
+
const config = core.getConfig({ projectRoot: workspaceRoot });
|
|
1283
|
+
const wuDir = parsedInput.data.wu_dir
|
|
1284
|
+
? path.resolve(workspaceRoot, parsedInput.data.wu_dir)
|
|
1285
|
+
: path.join(workspaceRoot, config.directories.wuDir);
|
|
1286
|
+
const stateDir = parsedInput.data.state_dir
|
|
1287
|
+
? path.resolve(workspaceRoot, parsedInput.data.state_dir)
|
|
1288
|
+
: path.join(workspaceRoot, config.state.stateDir);
|
|
1289
|
+
const dryRun = normalizeDryRun(parsedInput.data.execute, parsedInput.data.dry_run);
|
|
1290
|
+
const force = parsedInput.data.force ?? false;
|
|
1291
|
+
const eventsFilePath = path.join(stateDir, STATE_RUNTIME_CONSTANTS.WU_EVENTS_FILE_NAME);
|
|
1292
|
+
const wuDocuments = await loadWuLifecycleDocuments(core, wuDir);
|
|
1293
|
+
const bootstrapEvents = wuDocuments
|
|
1294
|
+
.flatMap((wu) => inferBootstrapEvents(core, wu))
|
|
1295
|
+
.sort((left, right) => {
|
|
1296
|
+
return new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime();
|
|
1297
|
+
});
|
|
1298
|
+
if (!dryRun) {
|
|
1299
|
+
const existingStateFile = await getPathInfo(eventsFilePath);
|
|
1300
|
+
if (existingStateFile.exists && !force) {
|
|
1301
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.STATE_BOOTSTRAP_FAILED, `State file already exists: ${eventsFilePath}. Use --force to overwrite.`);
|
|
1302
|
+
}
|
|
1303
|
+
await mkdir(stateDir, { recursive: true });
|
|
1304
|
+
const payload = bootstrapEvents.map((event) => JSON.stringify(event)).join('\n') +
|
|
1305
|
+
(bootstrapEvents.length > 0 ? '\n' : '');
|
|
1306
|
+
await writeFile(eventsFilePath, payload, UTF8_ENCODING);
|
|
1307
|
+
}
|
|
1308
|
+
return createSuccessOutput({
|
|
1309
|
+
dry_run: dryRun,
|
|
1310
|
+
events_generated: bootstrapEvents.length,
|
|
1311
|
+
events_written: dryRun ? 0 : bootstrapEvents.length,
|
|
1312
|
+
skipped: 0,
|
|
1313
|
+
warnings: wuDocuments.length === 0 ? [STATE_RUNTIME_MESSAGES.WU_DIRECTORY_EMPTY_OR_MISSING] : [],
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
catch (cause) {
|
|
1317
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.STATE_BOOTSTRAP_FAILED, cause.message);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
const stateCleanupInProcess = async (rawInput, context) => {
|
|
1321
|
+
const parsedInput = STATE_CLEANUP_INPUT_SCHEMA.safeParse(rawInput);
|
|
1322
|
+
if (!parsedInput.success) {
|
|
1323
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1324
|
+
}
|
|
1325
|
+
const exclusiveFlags = [
|
|
1326
|
+
parsedInput.data.signals_only,
|
|
1327
|
+
parsedInput.data.memory_only,
|
|
1328
|
+
parsedInput.data.events_only,
|
|
1329
|
+
].filter(Boolean);
|
|
1330
|
+
if (exclusiveFlags.length > 1) {
|
|
1331
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, STATE_RUNTIME_MESSAGES.MUTUALLY_EXCLUSIVE_CLEANUP_FLAGS);
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const core = await getCoreLazy();
|
|
1335
|
+
const memory = await getMemoryLazy();
|
|
1336
|
+
const projectRoot = resolveCommandBaseDir(context, parsedInput.data.base_dir);
|
|
1337
|
+
const result = await core.cleanupState(projectRoot, {
|
|
1338
|
+
dryRun: parsedInput.data.dry_run,
|
|
1339
|
+
signalsOnly: parsedInput.data.signals_only,
|
|
1340
|
+
memoryOnly: parsedInput.data.memory_only,
|
|
1341
|
+
eventsOnly: parsedInput.data.events_only,
|
|
1342
|
+
cleanupSignals: async (dir, options) => memory.cleanupSignals(dir, {
|
|
1343
|
+
dryRun: options.dryRun,
|
|
1344
|
+
getActiveWuIds: () => listActiveWuIds(dir),
|
|
1345
|
+
}),
|
|
1346
|
+
cleanupMemory: async (dir, options) => memory.cleanupMemory(dir, {
|
|
1347
|
+
dryRun: options.dryRun,
|
|
1348
|
+
}),
|
|
1349
|
+
archiveEvents: async (dir, options) => core.archiveWuEvents(dir, {
|
|
1350
|
+
dryRun: options.dryRun,
|
|
1351
|
+
}),
|
|
1352
|
+
});
|
|
1353
|
+
return createSuccessOutput(result);
|
|
1354
|
+
}
|
|
1355
|
+
catch (cause) {
|
|
1356
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.STATE_CLEANUP_FAILED, cause.message);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const stateDoctorInProcess = async (rawInput, context) => {
|
|
1360
|
+
const parsedInput = STATE_DOCTOR_INPUT_SCHEMA.safeParse(rawInput);
|
|
1361
|
+
if (!parsedInput.success) {
|
|
1362
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const core = await getCoreLazy();
|
|
1366
|
+
const projectRoot = resolveCommandBaseDir(context, parsedInput.data.base_dir);
|
|
1367
|
+
const deps = buildStateDoctorDeps(core, projectRoot);
|
|
1368
|
+
const diagnosis = await core.diagnoseState(projectRoot, deps, {
|
|
1369
|
+
fix: parsedInput.data.fix,
|
|
1370
|
+
dryRun: parsedInput.data.dry_run,
|
|
1371
|
+
});
|
|
1372
|
+
return createSuccessOutput(diagnosis);
|
|
1373
|
+
}
|
|
1374
|
+
catch (cause) {
|
|
1375
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.STATE_DOCTOR_FAILED, cause.message);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
const signalCleanupInProcess = async (rawInput, context) => {
|
|
1379
|
+
const parsedInput = SIGNAL_CLEANUP_INPUT_SCHEMA.safeParse(rawInput);
|
|
1380
|
+
if (!parsedInput.success) {
|
|
1381
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
const memory = await getMemoryLazy();
|
|
1385
|
+
const projectRoot = resolveCommandBaseDir(context, parsedInput.data.base_dir);
|
|
1386
|
+
const result = await memory.cleanupSignals(projectRoot, {
|
|
1387
|
+
dryRun: parsedInput.data.dry_run,
|
|
1388
|
+
ttl: parsedInput.data.ttl,
|
|
1389
|
+
unreadTtl: parsedInput.data.unread_ttl,
|
|
1390
|
+
maxEntries: parsedInput.data.max_entries,
|
|
1391
|
+
getActiveWuIds: () => listActiveWuIds(projectRoot),
|
|
1392
|
+
});
|
|
1393
|
+
return createSuccessOutput(result);
|
|
1394
|
+
}
|
|
1395
|
+
catch (cause) {
|
|
1396
|
+
return createFailureOutput(STATE_TOOL_ERROR_CODES.SIGNAL_CLEANUP_FAILED, cause.message);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
// --- WU-1802: Validation/Lane in-process handler implementations ---
|
|
1400
|
+
const VALIDATION_TOOL_ERROR_CODES = {
|
|
1401
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
1402
|
+
VALIDATE_ERROR: 'VALIDATE_ERROR',
|
|
1403
|
+
VALIDATE_AGENT_SKILLS_ERROR: 'VALIDATE_AGENT_SKILLS_ERROR',
|
|
1404
|
+
VALIDATE_AGENT_SYNC_ERROR: 'VALIDATE_AGENT_SYNC_ERROR',
|
|
1405
|
+
VALIDATE_BACKLOG_SYNC_ERROR: 'VALIDATE_BACKLOG_SYNC_ERROR',
|
|
1406
|
+
VALIDATE_SKILLS_SPEC_ERROR: 'VALIDATE_SKILLS_SPEC_ERROR',
|
|
1407
|
+
LUMENFLOW_VALIDATE_ERROR: 'LUMENFLOW_VALIDATE_ERROR',
|
|
1408
|
+
LANE_HEALTH_ERROR: 'LANE_HEALTH_ERROR',
|
|
1409
|
+
LANE_SUGGEST_ERROR: 'LANE_SUGGEST_ERROR',
|
|
1410
|
+
WU_STATUS_ERROR: 'WU_STATUS_ERROR',
|
|
1411
|
+
WU_CREATE_ERROR: 'WU_CREATE_ERROR',
|
|
1412
|
+
WU_CLAIM_ERROR: 'WU_CLAIM_ERROR',
|
|
1413
|
+
WU_PROTO_ERROR: 'WU_PROTO_ERROR',
|
|
1414
|
+
WU_DONE_ERROR: 'WU_DONE_ERROR',
|
|
1415
|
+
WU_PREP_ERROR: 'WU_PREP_ERROR',
|
|
1416
|
+
WU_SANDBOX_ERROR: 'WU_SANDBOX_ERROR',
|
|
1417
|
+
WU_PRUNE_ERROR: 'WU_PRUNE_ERROR',
|
|
1418
|
+
WU_DELETE_ERROR: 'WU_DELETE_ERROR',
|
|
1419
|
+
WU_CLEANUP_ERROR: 'WU_CLEANUP_ERROR',
|
|
1420
|
+
WU_BRIEF_ERROR: 'WU_BRIEF_ERROR',
|
|
1421
|
+
WU_DELEGATE_ERROR: 'WU_DELEGATE_ERROR',
|
|
1422
|
+
WU_UNLOCK_LANE_ERROR: 'WU_UNLOCK_LANE_ERROR',
|
|
1423
|
+
AGENT_SESSION_ERROR: 'AGENT_SESSION_ERROR',
|
|
1424
|
+
AGENT_SESSION_END_ERROR: 'AGENT_SESSION_END_ERROR',
|
|
1425
|
+
AGENT_LOG_ISSUE_ERROR: 'AGENT_LOG_ISSUE_ERROR',
|
|
1426
|
+
AGENT_ISSUES_QUERY_ERROR: 'AGENT_ISSUES_QUERY_ERROR',
|
|
1427
|
+
LUMENFLOW_INIT_ERROR: 'LUMENFLOW_INIT_ERROR',
|
|
1428
|
+
LUMENFLOW_DOCTOR_ERROR: 'LUMENFLOW_DOCTOR_ERROR',
|
|
1429
|
+
LUMENFLOW_INTEGRATE_ERROR: 'LUMENFLOW_INTEGRATE_ERROR',
|
|
1430
|
+
LUMENFLOW_UPGRADE_ERROR: 'LUMENFLOW_UPGRADE_ERROR',
|
|
1431
|
+
LUMENFLOW_RELEASE_ERROR: 'LUMENFLOW_RELEASE_ERROR',
|
|
1432
|
+
DOCS_SYNC_ERROR: 'DOCS_SYNC_ERROR',
|
|
1433
|
+
SYNC_TEMPLATES_ALIAS_ERROR: 'SYNC_TEMPLATES_ALIAS_ERROR',
|
|
1434
|
+
PLAN_CREATE_ERROR: 'PLAN_CREATE_ERROR',
|
|
1435
|
+
PLAN_EDIT_ERROR: 'PLAN_EDIT_ERROR',
|
|
1436
|
+
PLAN_LINK_ERROR: 'PLAN_LINK_ERROR',
|
|
1437
|
+
PLAN_PROMOTE_ERROR: 'PLAN_PROMOTE_ERROR',
|
|
1438
|
+
GATES_ERROR: 'GATES_ERROR',
|
|
1439
|
+
INITIATIVE_LIST_ERROR: 'INITIATIVE_LIST_ERROR',
|
|
1440
|
+
INITIATIVE_STATUS_ERROR: 'INITIATIVE_STATUS_ERROR',
|
|
1441
|
+
INITIATIVE_CREATE_ERROR: 'INITIATIVE_CREATE_ERROR',
|
|
1442
|
+
INITIATIVE_EDIT_ERROR: 'INITIATIVE_EDIT_ERROR',
|
|
1443
|
+
INITIATIVE_ADD_WU_ERROR: 'INITIATIVE_ADD_WU_ERROR',
|
|
1444
|
+
INITIATIVE_REMOVE_WU_ERROR: 'INITIATIVE_REMOVE_WU_ERROR',
|
|
1445
|
+
INITIATIVE_BULK_ASSIGN_ERROR: 'INITIATIVE_BULK_ASSIGN_ERROR',
|
|
1446
|
+
INITIATIVE_PLAN_ERROR: 'INITIATIVE_PLAN_ERROR',
|
|
1447
|
+
INIT_PLAN_ERROR: 'INIT_PLAN_ERROR',
|
|
1448
|
+
ORCHESTRATE_INITIATIVE_ERROR: 'ORCHESTRATE_INITIATIVE_ERROR',
|
|
1449
|
+
MEM_INIT_ERROR: 'MEM_INIT_ERROR',
|
|
1450
|
+
MEM_START_ERROR: 'MEM_START_ERROR',
|
|
1451
|
+
MEM_READY_ERROR: 'MEM_READY_ERROR',
|
|
1452
|
+
MEM_CHECKPOINT_ERROR: 'MEM_CHECKPOINT_ERROR',
|
|
1453
|
+
MEM_CLEANUP_ERROR: 'MEM_CLEANUP_ERROR',
|
|
1454
|
+
MEM_CONTEXT_ERROR: 'MEM_CONTEXT_ERROR',
|
|
1455
|
+
MEM_CREATE_ERROR: 'MEM_CREATE_ERROR',
|
|
1456
|
+
MEM_DELETE_ERROR: 'MEM_DELETE_ERROR',
|
|
1457
|
+
MEM_EXPORT_ERROR: 'MEM_EXPORT_ERROR',
|
|
1458
|
+
MEM_INBOX_ERROR: 'MEM_INBOX_ERROR',
|
|
1459
|
+
MEM_SIGNAL_ERROR: 'MEM_SIGNAL_ERROR',
|
|
1460
|
+
MEM_SUMMARIZE_ERROR: 'MEM_SUMMARIZE_ERROR',
|
|
1461
|
+
MEM_TRIAGE_ERROR: 'MEM_TRIAGE_ERROR',
|
|
1462
|
+
MEM_RECOVER_ERROR: 'MEM_RECOVER_ERROR',
|
|
1463
|
+
WU_BLOCK_ERROR: 'WU_BLOCK_ERROR',
|
|
1464
|
+
WU_UNBLOCK_ERROR: 'WU_UNBLOCK_ERROR',
|
|
1465
|
+
WU_EDIT_ERROR: 'WU_EDIT_ERROR',
|
|
1466
|
+
WU_RELEASE_ERROR: 'WU_RELEASE_ERROR',
|
|
1467
|
+
WU_RECOVER_ERROR: 'WU_RECOVER_ERROR',
|
|
1468
|
+
WU_REPAIR_ERROR: 'WU_REPAIR_ERROR',
|
|
1469
|
+
WU_DEPS_ERROR: 'WU_DEPS_ERROR',
|
|
1470
|
+
WU_PREFLIGHT_ERROR: 'WU_PREFLIGHT_ERROR',
|
|
1471
|
+
WU_VALIDATE_ERROR: 'WU_VALIDATE_ERROR',
|
|
1472
|
+
WU_INFER_LANE_ERROR: 'WU_INFER_LANE_ERROR',
|
|
1473
|
+
MISSING_PARAMETER: 'MISSING_PARAMETER',
|
|
1474
|
+
};
|
|
1475
|
+
const VALIDATION_TOOL_MESSAGES = {
|
|
1476
|
+
VALIDATE_PASSED: 'Validation passed',
|
|
1477
|
+
VALIDATE_INVALID_WU: 'Invalid WU',
|
|
1478
|
+
NO_WU_DIR: 'No WU directory found, skipping',
|
|
1479
|
+
NO_SKILLS_DIR: 'No skills directory found, skipping',
|
|
1480
|
+
NO_AGENTS_DIR: 'No agents directory found, skipping',
|
|
1481
|
+
AGENT_SKILLS_FAILED: 'Agent skills validation failed',
|
|
1482
|
+
AGENT_SYNC_FAILED: 'Agent sync validation failed',
|
|
1483
|
+
EMPTY_FILE: 'empty file',
|
|
1484
|
+
EMPTY_AGENT_CONFIG: 'empty agent config',
|
|
1485
|
+
EMPTY_SKILLS_SPEC: 'empty skills spec',
|
|
1486
|
+
BACKLOG_SYNC_VALID: 'Backlog sync valid',
|
|
1487
|
+
BACKLOG_SYNC_FAILED: 'Backlog sync validation failed',
|
|
1488
|
+
SKILLS_SPEC_FAILED: 'Skills spec validation failed',
|
|
1489
|
+
LANE_HEALTH_PASSED: 'Lane health check complete',
|
|
1490
|
+
WU_BLOCK_PASSED: 'WU blocked successfully',
|
|
1491
|
+
WU_UNBLOCK_PASSED: 'WU unblocked successfully',
|
|
1492
|
+
WU_EDIT_PASSED: 'WU edited successfully',
|
|
1493
|
+
WU_RELEASE_PASSED: 'WU released successfully',
|
|
1494
|
+
WU_RELEASE_NO_REASON: 'No reason provided',
|
|
1495
|
+
};
|
|
1496
|
+
/** WU-1856: Single function replaces PREFIX/SUFFIX constant fragmentation. */
|
|
1497
|
+
function validationCountMsg(label, count) {
|
|
1498
|
+
return `${label}: ${count} checked`;
|
|
1499
|
+
}
|
|
1500
|
+
const VALIDATION_TOOL_FILE_EXTENSIONS = ['.md', '.yaml', '.yml'];
|
|
1501
|
+
const WU_FILE_EXTENSIONS = ['.yaml', '.yml'];
|
|
1502
|
+
const SKILLS_DIR_RELATIVE = '.claude/skills';
|
|
1503
|
+
const AGENTS_DIR_RELATIVE = '.claude/agents';
|
|
1504
|
+
const VALIDATE_INPUT_SCHEMA = z.object({
|
|
1505
|
+
id: z.string().optional(),
|
|
1506
|
+
strict: z.boolean().optional(),
|
|
1507
|
+
done_only: z.boolean().optional(),
|
|
1508
|
+
});
|
|
1509
|
+
const VALIDATE_AGENT_SKILLS_INPUT_SCHEMA = z.object({
|
|
1510
|
+
skill: z.string().optional(),
|
|
1511
|
+
});
|
|
1512
|
+
const VALIDATE_AGENT_SYNC_INPUT_SCHEMA = z.object({});
|
|
1513
|
+
const VALIDATE_BACKLOG_SYNC_INPUT_SCHEMA = z.object({});
|
|
1514
|
+
const VALIDATE_SKILLS_SPEC_INPUT_SCHEMA = z.object({});
|
|
1515
|
+
const LANE_HEALTH_INPUT_SCHEMA = z.object({
|
|
1516
|
+
json: z.boolean().optional(),
|
|
1517
|
+
verbose: z.boolean().optional(),
|
|
1518
|
+
no_coverage: z.boolean().optional(),
|
|
1519
|
+
});
|
|
1520
|
+
const LANE_SUGGEST_INPUT_SCHEMA = z.object({
|
|
1521
|
+
dry_run: z.boolean().optional(),
|
|
1522
|
+
interactive: z.boolean().optional(),
|
|
1523
|
+
output: z.string().optional(),
|
|
1524
|
+
json: z.boolean().optional(),
|
|
1525
|
+
no_llm: z.boolean().optional(),
|
|
1526
|
+
include_git: z.boolean().optional(),
|
|
1527
|
+
});
|
|
1528
|
+
/** Helper: filter files by validation-relevant extensions */
|
|
1529
|
+
function hasValidationExtension(filename) {
|
|
1530
|
+
return VALIDATION_TOOL_FILE_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
1531
|
+
}
|
|
1532
|
+
/** Helper: filter files by WU YAML extensions */
|
|
1533
|
+
function hasWUExtension(filename) {
|
|
1534
|
+
return WU_FILE_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
1535
|
+
}
|
|
1536
|
+
/** Helper: extract Zod issue messages from safeParse error */
|
|
1537
|
+
function formatZodIssues(zodError) {
|
|
1538
|
+
return (zodError.issues?.map((i) => i.message).join('; ') ??
|
|
1539
|
+
VALIDATION_TOOL_MESSAGES.VALIDATE_INVALID_WU);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* validate handler — delegates to @lumenflow/core validateWU per file
|
|
1543
|
+
*/
|
|
1544
|
+
const validateInProcess = async (rawInput, context) => {
|
|
1545
|
+
const parsedInput = VALIDATE_INPUT_SCHEMA.safeParse(rawInput ?? {});
|
|
1546
|
+
if (!parsedInput.success) {
|
|
1547
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const core = await getCoreLazy();
|
|
1551
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1552
|
+
const config = core.getConfig({ projectRoot });
|
|
1553
|
+
const wuDir = path.join(projectRoot, config.directories.wuDir);
|
|
1554
|
+
if (parsedInput.data.id) {
|
|
1555
|
+
const wuPath = path.join(wuDir, `${parsedInput.data.id}.yaml`);
|
|
1556
|
+
const result = core.validateWU(core.parseYAML(await readFile(wuPath, UTF8_ENCODING)));
|
|
1557
|
+
return result.success
|
|
1558
|
+
? createSuccessOutput({
|
|
1559
|
+
message: `${parsedInput.data.id} ${VALIDATION_TOOL_MESSAGES.VALIDATE_PASSED}`,
|
|
1560
|
+
})
|
|
1561
|
+
: createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_ERROR, formatZodIssues(result.error));
|
|
1562
|
+
}
|
|
1563
|
+
// validateAllWUs is not exported from core — inline the aggregation
|
|
1564
|
+
let files;
|
|
1565
|
+
try {
|
|
1566
|
+
files = (await readdir(wuDir)).filter(hasWUExtension);
|
|
1567
|
+
}
|
|
1568
|
+
catch {
|
|
1569
|
+
return createSuccessOutput({ message: VALIDATION_TOOL_MESSAGES.NO_WU_DIR });
|
|
1570
|
+
}
|
|
1571
|
+
let totalValid = 0;
|
|
1572
|
+
let totalInvalid = 0;
|
|
1573
|
+
const errors = [];
|
|
1574
|
+
const STATUS_DONE = 'done';
|
|
1575
|
+
for (const file of files) {
|
|
1576
|
+
const content = await readFile(path.join(wuDir, file), UTF8_ENCODING);
|
|
1577
|
+
const parsed = core.parseYAML(content);
|
|
1578
|
+
if (parsedInput.data.done_only) {
|
|
1579
|
+
if (parsed &&
|
|
1580
|
+
typeof parsed === 'object' &&
|
|
1581
|
+
'status' in parsed &&
|
|
1582
|
+
parsed.status !== STATUS_DONE)
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const result = core.validateWU(parsed);
|
|
1586
|
+
if (result.success) {
|
|
1587
|
+
totalValid++;
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
totalInvalid++;
|
|
1591
|
+
errors.push(`${file}: ${formatZodIssues(result.error)}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (totalInvalid === 0) {
|
|
1595
|
+
return createSuccessOutput({
|
|
1596
|
+
message: `${VALIDATION_TOOL_MESSAGES.VALIDATE_PASSED}: ${totalValid} valid`,
|
|
1597
|
+
totalValid,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_ERROR, `${totalInvalid} invalid of ${totalValid + totalInvalid} total\n${errors.join('\n')}`);
|
|
1601
|
+
}
|
|
1602
|
+
catch (err) {
|
|
1603
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_ERROR, err.message);
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
/**
|
|
1607
|
+
* validate:agent-skills handler — scans skill YAML files for required fields
|
|
1608
|
+
*/
|
|
1609
|
+
const validateAgentSkillsInProcess = async (rawInput, context) => {
|
|
1610
|
+
const parsedInput = VALIDATE_AGENT_SKILLS_INPUT_SCHEMA.safeParse(rawInput ?? {});
|
|
1611
|
+
if (!parsedInput.success) {
|
|
1612
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1616
|
+
const skillsDir = path.join(projectRoot, SKILLS_DIR_RELATIVE);
|
|
1617
|
+
let skillFiles;
|
|
1618
|
+
try {
|
|
1619
|
+
skillFiles = (await readdir(skillsDir)).filter(hasValidationExtension);
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1622
|
+
return createSuccessOutput({ message: VALIDATION_TOOL_MESSAGES.NO_SKILLS_DIR, valid: true });
|
|
1623
|
+
}
|
|
1624
|
+
if (parsedInput.data.skill) {
|
|
1625
|
+
skillFiles = skillFiles.filter((f) => f.includes(parsedInput.data.skill));
|
|
1626
|
+
}
|
|
1627
|
+
const issues = [];
|
|
1628
|
+
for (const file of skillFiles) {
|
|
1629
|
+
const content = await readFile(path.join(skillsDir, file), UTF8_ENCODING);
|
|
1630
|
+
if (content.trim().length === 0) {
|
|
1631
|
+
issues.push(`${file}: ${VALIDATION_TOOL_MESSAGES.EMPTY_FILE}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (issues.length > 0) {
|
|
1635
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_AGENT_SKILLS_ERROR, `${VALIDATION_TOOL_MESSAGES.AGENT_SKILLS_FAILED}:\n${issues.join('\n')}`);
|
|
1636
|
+
}
|
|
1637
|
+
return createSuccessOutput({
|
|
1638
|
+
message: validationCountMsg('Agent skills valid', skillFiles.length),
|
|
1639
|
+
valid: true,
|
|
1640
|
+
count: skillFiles.length,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
catch (err) {
|
|
1644
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_AGENT_SKILLS_ERROR, err.message);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
/**
|
|
1648
|
+
* validate:agent-sync handler — checks agent config files are in sync
|
|
1649
|
+
*/
|
|
1650
|
+
const validateAgentSyncInProcess = async (_rawInput, context) => {
|
|
1651
|
+
try {
|
|
1652
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1653
|
+
const agentsDir = path.join(projectRoot, AGENTS_DIR_RELATIVE);
|
|
1654
|
+
let agentFiles;
|
|
1655
|
+
try {
|
|
1656
|
+
agentFiles = (await readdir(agentsDir)).filter(hasValidationExtension);
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
return createSuccessOutput({ message: VALIDATION_TOOL_MESSAGES.NO_AGENTS_DIR, valid: true });
|
|
1660
|
+
}
|
|
1661
|
+
const issues = [];
|
|
1662
|
+
for (const file of agentFiles) {
|
|
1663
|
+
const content = await readFile(path.join(agentsDir, file), UTF8_ENCODING);
|
|
1664
|
+
if (content.trim().length === 0) {
|
|
1665
|
+
issues.push(`${file}: ${VALIDATION_TOOL_MESSAGES.EMPTY_AGENT_CONFIG}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (issues.length > 0) {
|
|
1669
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_AGENT_SYNC_ERROR, `${VALIDATION_TOOL_MESSAGES.AGENT_SYNC_FAILED}:\n${issues.join('\n')}`);
|
|
1670
|
+
}
|
|
1671
|
+
return createSuccessOutput({
|
|
1672
|
+
message: validationCountMsg('Agent sync valid', agentFiles.length),
|
|
1673
|
+
valid: true,
|
|
1674
|
+
count: agentFiles.length,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_AGENT_SYNC_ERROR, err.message);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
/**
|
|
1682
|
+
* validate:backlog-sync handler — delegates to @lumenflow/core validateBacklogSync
|
|
1683
|
+
*/
|
|
1684
|
+
const validateBacklogSyncInProcess = async (_rawInput, context) => {
|
|
1685
|
+
try {
|
|
1686
|
+
const core = await getCoreLazy();
|
|
1687
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1688
|
+
const config = core.getConfig({ projectRoot });
|
|
1689
|
+
const backlogFilePath = path.join(projectRoot, config.directories.backlogPath);
|
|
1690
|
+
const result = core.validateBacklogSync(backlogFilePath);
|
|
1691
|
+
if (result.valid) {
|
|
1692
|
+
return createSuccessOutput({
|
|
1693
|
+
message: VALIDATION_TOOL_MESSAGES.BACKLOG_SYNC_VALID,
|
|
1694
|
+
...result,
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_BACKLOG_SYNC_ERROR, result.errors?.join('\n') ?? VALIDATION_TOOL_MESSAGES.BACKLOG_SYNC_FAILED);
|
|
1698
|
+
}
|
|
1699
|
+
catch (err) {
|
|
1700
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_BACKLOG_SYNC_ERROR, err.message);
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
/**
|
|
1704
|
+
* validate:skills-spec handler — validates skills specification YAML files
|
|
1705
|
+
*/
|
|
1706
|
+
const validateSkillsSpecInProcess = async (_rawInput, context) => {
|
|
1707
|
+
try {
|
|
1708
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1709
|
+
const skillsDir = path.join(projectRoot, SKILLS_DIR_RELATIVE);
|
|
1710
|
+
let skillFiles;
|
|
1711
|
+
try {
|
|
1712
|
+
skillFiles = (await readdir(skillsDir)).filter(hasValidationExtension);
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
return createSuccessOutput({ message: VALIDATION_TOOL_MESSAGES.NO_SKILLS_DIR, valid: true });
|
|
1716
|
+
}
|
|
1717
|
+
const issues = [];
|
|
1718
|
+
for (const file of skillFiles) {
|
|
1719
|
+
const filePath = path.join(skillsDir, file);
|
|
1720
|
+
const fileStat = await stat(filePath);
|
|
1721
|
+
if (fileStat.size === 0) {
|
|
1722
|
+
issues.push(`${file}: ${VALIDATION_TOOL_MESSAGES.EMPTY_SKILLS_SPEC}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (issues.length > 0) {
|
|
1726
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_SKILLS_SPEC_ERROR, `${VALIDATION_TOOL_MESSAGES.SKILLS_SPEC_FAILED}:\n${issues.join('\n')}`);
|
|
1727
|
+
}
|
|
1728
|
+
return createSuccessOutput({
|
|
1729
|
+
message: validationCountMsg('Skills spec valid', skillFiles.length),
|
|
1730
|
+
valid: true,
|
|
1731
|
+
count: skillFiles.length,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
catch (err) {
|
|
1735
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.VALIDATE_SKILLS_SPEC_ERROR, err.message);
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
/**
|
|
1739
|
+
* lane:health handler — delegates to @lumenflow/core lane checker
|
|
1740
|
+
*/
|
|
1741
|
+
const laneHealthInProcess = async (rawInput, context) => {
|
|
1742
|
+
const parsedInput = LANE_HEALTH_INPUT_SCHEMA.safeParse(rawInput ?? {});
|
|
1743
|
+
if (!parsedInput.success) {
|
|
1744
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
const core = await getCoreLazy();
|
|
1748
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1749
|
+
const config = core.getConfig({ projectRoot });
|
|
1750
|
+
const laneConfigMap = resolveLanePolicyConfig(config);
|
|
1751
|
+
const allWUs = await core.listWUs({ projectRoot });
|
|
1752
|
+
const laneOccupancy = {};
|
|
1753
|
+
for (const wu of allWUs) {
|
|
1754
|
+
if (!wu.lane)
|
|
1755
|
+
continue;
|
|
1756
|
+
const existing = laneOccupancy[wu.lane];
|
|
1757
|
+
const entry = existing ?? { in_progress: [], blocked: [] };
|
|
1758
|
+
if (!existing) {
|
|
1759
|
+
laneOccupancy[wu.lane] = entry;
|
|
1760
|
+
}
|
|
1761
|
+
if (wu.status === STATUS_IN_PROGRESS) {
|
|
1762
|
+
entry.in_progress.push(wu.id);
|
|
1763
|
+
}
|
|
1764
|
+
else if (wu.status === STATUS_BLOCKED) {
|
|
1765
|
+
entry.blocked.push(wu.id);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const overlaps = [];
|
|
1769
|
+
const lanes = Object.keys(laneConfigMap);
|
|
1770
|
+
for (const lane of lanes) {
|
|
1771
|
+
const occupancy = laneOccupancy[lane];
|
|
1772
|
+
const lanePolicy = laneConfigMap[lane];
|
|
1773
|
+
if (!occupancy || !lanePolicy)
|
|
1774
|
+
continue;
|
|
1775
|
+
if (lanePolicy.lockPolicy !== LOCK_POLICY_NONE &&
|
|
1776
|
+
occupancy.in_progress.length > lanePolicy.wipLimit) {
|
|
1777
|
+
overlaps.push(`${lane}: ${occupancy.in_progress.length} in-progress (limit ${lanePolicy.wipLimit})`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const healthResult = {
|
|
1781
|
+
lanes: Object.keys(laneConfigMap).length,
|
|
1782
|
+
occupied: Object.keys(laneOccupancy).length,
|
|
1783
|
+
overlaps,
|
|
1784
|
+
lane_details: Object.entries(laneConfigMap).map(([name, cfg]) => ({
|
|
1785
|
+
name,
|
|
1786
|
+
policy: cfg.lockPolicy,
|
|
1787
|
+
wip_limit: cfg.wipLimit,
|
|
1788
|
+
in_progress: laneOccupancy[name]?.in_progress.length ?? 0,
|
|
1789
|
+
blocked: laneOccupancy[name]?.blocked.length ?? 0,
|
|
1790
|
+
})),
|
|
1791
|
+
};
|
|
1792
|
+
if (overlaps.length > 0) {
|
|
1793
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LANE_HEALTH_ERROR, overlaps.join('\n'));
|
|
1794
|
+
}
|
|
1795
|
+
return createSuccessOutput({
|
|
1796
|
+
message: VALIDATION_TOOL_MESSAGES.LANE_HEALTH_PASSED,
|
|
1797
|
+
...healthResult,
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
catch (err) {
|
|
1801
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LANE_HEALTH_ERROR, err.message);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
/**
|
|
1805
|
+
* lane:suggest handler — generates lane suggestions from codebase context
|
|
1806
|
+
*/
|
|
1807
|
+
const laneSuggestInProcess = async (rawInput, context) => {
|
|
1808
|
+
const parsedInput = LANE_SUGGEST_INPUT_SCHEMA.safeParse(rawInput ?? {});
|
|
1809
|
+
if (!parsedInput.success) {
|
|
1810
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.error.message);
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const core = await getCoreLazy();
|
|
1814
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1815
|
+
const config = core.getConfig({ projectRoot });
|
|
1816
|
+
const laneConfigMap = resolveLanePolicyConfig(config);
|
|
1817
|
+
const existingLanes = Object.keys(laneConfigMap);
|
|
1818
|
+
// WU-1856: Use core's real filesystem scanner instead of empty stub.
|
|
1819
|
+
// Override existingLanes with policy-resolved lanes (more authoritative).
|
|
1820
|
+
const projectContext = core.gatherProjectContext(projectRoot);
|
|
1821
|
+
projectContext.existingLanes = existingLanes;
|
|
1822
|
+
const suggestions = core.getDefaultSuggestions(projectContext);
|
|
1823
|
+
return createSuccessOutput({
|
|
1824
|
+
message: validationCountMsg('Lane suggestions generated', suggestions.length),
|
|
1825
|
+
suggestions,
|
|
1826
|
+
existing_lanes: existingLanes,
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
catch (err) {
|
|
1830
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LANE_SUGGEST_ERROR, err.message);
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
/**
|
|
1834
|
+
* WU-1805: WU query in-process handlers
|
|
1835
|
+
*/
|
|
1836
|
+
const WU_QUERY_MESSAGES = {
|
|
1837
|
+
ID_REQUIRED: 'id parameter is required',
|
|
1838
|
+
RUNTIME_CLI_FALLBACK: 'Runtime in-process path not available; falling back to CLI',
|
|
1839
|
+
STATUS_FAILED: 'wu:status failed',
|
|
1840
|
+
PREFLIGHT_PASSED: 'Preflight checks passed',
|
|
1841
|
+
PREFLIGHT_FAILED: 'wu:preflight failed',
|
|
1842
|
+
VALIDATE_PASSED: 'WU is valid',
|
|
1843
|
+
VALIDATE_FAILED: 'wu:validate failed',
|
|
1844
|
+
INFER_LANE_FAILED: 'wu:infer-lane failed',
|
|
1845
|
+
};
|
|
1846
|
+
const lumenflowInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LUMENFLOW_INIT_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1847
|
+
const lumenflowDoctorInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LUMENFLOW_DOCTOR_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1848
|
+
const lumenflowIntegrateInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LUMENFLOW_INTEGRATE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1849
|
+
const lumenflowUpgradeInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LUMENFLOW_UPGRADE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1850
|
+
const lumenflowReleaseInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.LUMENFLOW_RELEASE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1851
|
+
const docsSyncInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.DOCS_SYNC_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1852
|
+
const syncTemplatesInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.SYNC_TEMPLATES_ALIAS_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1853
|
+
const planCreateInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.PLAN_CREATE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1854
|
+
const planEditInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.PLAN_EDIT_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1855
|
+
const planLinkInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.PLAN_LINK_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1856
|
+
const planPromoteInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.PLAN_PROMOTE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1857
|
+
const initiativeListInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_LIST_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1858
|
+
const initiativeStatusInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_STATUS_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1859
|
+
const initiativeCreateInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_CREATE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1860
|
+
const initiativeEditInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_EDIT_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1861
|
+
const initiativeAddWuInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_ADD_WU_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1862
|
+
const initiativeRemoveWuInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_REMOVE_WU_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1863
|
+
const initiativeBulkAssignInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_BULK_ASSIGN_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1864
|
+
const initiativePlanInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INITIATIVE_PLAN_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1865
|
+
const initPlanInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.INIT_PLAN_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1866
|
+
const orchestrateInitiativeInProcess = async () => createFailureOutput(VALIDATION_TOOL_ERROR_CODES.ORCHESTRATE_INITIATIVE_ERROR, WU_QUERY_MESSAGES.RUNTIME_CLI_FALLBACK);
|
|
1867
|
+
const wuInferLaneInProcess = async (rawInput, context) => {
|
|
1868
|
+
const input = (rawInput ?? {});
|
|
1869
|
+
try {
|
|
1870
|
+
const core = await getCoreLazy();
|
|
1871
|
+
const projectRoot = resolveWorkspaceRoot(context);
|
|
1872
|
+
let codePaths = [];
|
|
1873
|
+
let description = '';
|
|
1874
|
+
if (Array.isArray(input.paths)) {
|
|
1875
|
+
codePaths = input.paths.filter((p) => typeof p === 'string');
|
|
1876
|
+
}
|
|
1877
|
+
if (typeof input.desc === 'string') {
|
|
1878
|
+
description = input.desc;
|
|
1879
|
+
}
|
|
1880
|
+
// If id provided and no explicit paths, read from WU YAML
|
|
1881
|
+
if (typeof input.id === 'string' && codePaths.length === 0) {
|
|
1882
|
+
const wuFile = path.join(projectRoot, 'docs/04-operations/tasks/wu', `${input.id}.yaml`);
|
|
1883
|
+
try {
|
|
1884
|
+
const content = await readFile(wuFile, UTF8_ENCODING);
|
|
1885
|
+
const parsed = core.parseYAML(content);
|
|
1886
|
+
if (parsed && typeof parsed === 'object') {
|
|
1887
|
+
const wuData = parsed;
|
|
1888
|
+
if (Array.isArray(wuData.code_paths)) {
|
|
1889
|
+
codePaths = wuData.code_paths.filter((p) => typeof p === 'string');
|
|
1890
|
+
}
|
|
1891
|
+
if (!description && typeof wuData.description === 'string') {
|
|
1892
|
+
description = wuData.description;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
// WU file not found or unreadable — continue with provided inputs
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const result = core.inferSubLane(codePaths, description);
|
|
1901
|
+
return createSuccessOutput({ lane: result.lane, confidence: result.confidence });
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
return createFailureOutput(VALIDATION_TOOL_ERROR_CODES.WU_INFER_LANE_ERROR, err.message);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
// WU-1897: These in-process implementations are retained temporarily for
|
|
1908
|
+
// reference parity but are intentionally removed from resolver registration.
|
|
1909
|
+
const retiredWu1897InProcessHandlers = [
|
|
1910
|
+
lumenflowInProcess,
|
|
1911
|
+
lumenflowDoctorInProcess,
|
|
1912
|
+
lumenflowIntegrateInProcess,
|
|
1913
|
+
lumenflowUpgradeInProcess,
|
|
1914
|
+
lumenflowReleaseInProcess,
|
|
1915
|
+
docsSyncInProcess,
|
|
1916
|
+
syncTemplatesInProcess,
|
|
1917
|
+
planCreateInProcess,
|
|
1918
|
+
planEditInProcess,
|
|
1919
|
+
planLinkInProcess,
|
|
1920
|
+
planPromoteInProcess,
|
|
1921
|
+
initiativeListInProcess,
|
|
1922
|
+
initiativeStatusInProcess,
|
|
1923
|
+
initiativeCreateInProcess,
|
|
1924
|
+
initiativeEditInProcess,
|
|
1925
|
+
initiativeAddWuInProcess,
|
|
1926
|
+
initiativeRemoveWuInProcess,
|
|
1927
|
+
initiativeBulkAssignInProcess,
|
|
1928
|
+
initiativePlanInProcess,
|
|
1929
|
+
initPlanInProcess,
|
|
1930
|
+
orchestrateInitiativeInProcess,
|
|
1931
|
+
orchestrateInitStatusInProcess,
|
|
1932
|
+
orchestrateMonitorInProcess,
|
|
1933
|
+
delegationListInProcess,
|
|
1934
|
+
];
|
|
1935
|
+
void retiredWu1897InProcessHandlers;
|
|
1936
|
+
// WU-1890: Remaining file/git/state/validation/lane surfaces are now migrated to
|
|
1937
|
+
// pack handler implementations. Keep legacy implementations for reference parity.
|
|
1938
|
+
const retiredWu1890InProcessHandlers = [
|
|
1939
|
+
wuInferLaneInProcess,
|
|
1940
|
+
fileReadInProcess,
|
|
1941
|
+
fileWriteInProcess,
|
|
1942
|
+
fileEditInProcess,
|
|
1943
|
+
fileDeleteInProcess,
|
|
1944
|
+
backlogPruneInProcess,
|
|
1945
|
+
stateBootstrapInProcess,
|
|
1946
|
+
stateCleanupInProcess,
|
|
1947
|
+
stateDoctorInProcess,
|
|
1948
|
+
signalCleanupInProcess,
|
|
1949
|
+
validateInProcess,
|
|
1950
|
+
validateAgentSkillsInProcess,
|
|
1951
|
+
validateAgentSyncInProcess,
|
|
1952
|
+
validateBacklogSyncInProcess,
|
|
1953
|
+
validateSkillsSpecInProcess,
|
|
1954
|
+
laneHealthInProcess,
|
|
1955
|
+
laneSuggestInProcess,
|
|
1956
|
+
];
|
|
1957
|
+
void retiredWu1890InProcessHandlers;
|
|
1958
|
+
const retiredWu1890InProcessSchemas = [
|
|
1959
|
+
DEFAULT_IN_PROCESS_OUTPUT_SCHEMA,
|
|
1960
|
+
VALIDATE_INPUT_SCHEMA,
|
|
1961
|
+
FILE_READ_INPUT_SCHEMA,
|
|
1962
|
+
FILE_READ_OUTPUT_SCHEMA,
|
|
1963
|
+
FILE_WRITE_INPUT_SCHEMA,
|
|
1964
|
+
FILE_WRITE_OUTPUT_SCHEMA,
|
|
1965
|
+
FILE_EDIT_INPUT_SCHEMA,
|
|
1966
|
+
FILE_EDIT_OUTPUT_SCHEMA,
|
|
1967
|
+
FILE_DELETE_INPUT_SCHEMA,
|
|
1968
|
+
FILE_DELETE_OUTPUT_SCHEMA,
|
|
1969
|
+
BACKLOG_PRUNE_INPUT_SCHEMA,
|
|
1970
|
+
STATE_BOOTSTRAP_INPUT_SCHEMA,
|
|
1971
|
+
STATE_CLEANUP_INPUT_SCHEMA,
|
|
1972
|
+
STATE_DOCTOR_INPUT_SCHEMA,
|
|
1973
|
+
SIGNAL_CLEANUP_INPUT_SCHEMA,
|
|
1974
|
+
VALIDATE_AGENT_SKILLS_INPUT_SCHEMA,
|
|
1975
|
+
VALIDATE_AGENT_SYNC_INPUT_SCHEMA,
|
|
1976
|
+
VALIDATE_BACKLOG_SYNC_INPUT_SCHEMA,
|
|
1977
|
+
VALIDATE_SKILLS_SPEC_INPUT_SCHEMA,
|
|
1978
|
+
LANE_HEALTH_INPUT_SCHEMA,
|
|
1979
|
+
LANE_SUGGEST_INPUT_SCHEMA,
|
|
1980
|
+
];
|
|
1981
|
+
void retiredWu1890InProcessSchemas;
|
|
1982
|
+
const registeredInProcessToolHandlers = new Map([
|
|
1983
|
+
// WU-1905: flow:bottlenecks, flow:report, metrics:snapshot, metrics, and lumenflow:metrics
|
|
1984
|
+
// have been migrated to pack handler implementations. Their resolver registrations
|
|
1985
|
+
// are removed; they now execute through the pack handler path.
|
|
1986
|
+
[
|
|
1987
|
+
'context:get',
|
|
1988
|
+
{
|
|
1989
|
+
description: 'Get current LumenFlow context via in-process computation',
|
|
1990
|
+
inputSchema: DEFAULT_IN_PROCESS_INPUT_SCHEMA,
|
|
1991
|
+
fn: contextGetHandler,
|
|
1992
|
+
},
|
|
1993
|
+
],
|
|
1994
|
+
[
|
|
1995
|
+
'wu:list',
|
|
1996
|
+
{
|
|
1997
|
+
description: 'List WUs via in-process state store + YAML merge',
|
|
1998
|
+
inputSchema: DEFAULT_IN_PROCESS_INPUT_SCHEMA,
|
|
1999
|
+
fn: wuListHandler,
|
|
2000
|
+
},
|
|
2001
|
+
],
|
|
2002
|
+
]);
|
|
2003
|
+
export function isInProcessPackToolRegistered(toolName) {
|
|
2004
|
+
return registeredInProcessToolHandlers.has(toolName);
|
|
2005
|
+
}
|
|
2006
|
+
export function listInProcessPackTools() {
|
|
2007
|
+
return [...registeredInProcessToolHandlers.keys()].sort();
|
|
2008
|
+
}
|
|
2009
|
+
export const packToolCapabilityResolver = async (input) => {
|
|
2010
|
+
const registeredHandler = registeredInProcessToolHandlers.get(input.tool.name);
|
|
2011
|
+
if (!registeredHandler) {
|
|
2012
|
+
return defaultRuntimeToolCapabilityResolver(input);
|
|
2013
|
+
}
|
|
2014
|
+
return {
|
|
2015
|
+
name: input.tool.name,
|
|
2016
|
+
domain: input.loadedPack.manifest.id,
|
|
2017
|
+
version: input.loadedPack.manifest.version,
|
|
2018
|
+
input_schema: registeredHandler.inputSchema,
|
|
2019
|
+
output_schema: registeredHandler.outputSchema,
|
|
2020
|
+
permission: input.tool.permission,
|
|
2021
|
+
required_scopes: input.tool.required_scopes,
|
|
2022
|
+
handler: {
|
|
2023
|
+
kind: TOOL_HANDLER_KINDS.IN_PROCESS,
|
|
2024
|
+
fn: registeredHandler.fn,
|
|
2025
|
+
},
|
|
2026
|
+
description: registeredHandler.description,
|
|
2027
|
+
pack: input.loadedPack.pin.id,
|
|
2028
|
+
};
|
|
2029
|
+
};
|
|
2030
|
+
//# sourceMappingURL=runtime-tool-resolver.js.map
|