zigrix 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.js +8 -1
- package/dist/config/schema.d.ts +112 -0
- package/dist/config/schema.js +186 -12
- package/dist/dashboard/.next/BUILD_ID +1 -1
- package/dist/dashboard/.next/app-build-manifest.json +10 -10
- package/dist/dashboard/.next/app-path-routes-manifest.json +2 -2
- package/dist/dashboard/.next/build-manifest.json +2 -2
- package/dist/dashboard/.next/prerender-manifest.json +6 -6
- package/dist/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/_not-found.html +1 -1
- package/dist/dashboard/.next/server/app/_not-found.rsc +1 -1
- package/dist/dashboard/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/session/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/overview/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/conversation/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/login.html +1 -1
- package/dist/dashboard/.next/server/app/login.rsc +1 -1
- package/dist/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/setup.html +1 -1
- package/dist/dashboard/.next/server/app/setup.rsc +1 -1
- package/dist/dashboard/.next/server/app-paths-manifest.json +2 -2
- package/dist/dashboard/.next/server/functions-config-manifest.json +2 -2
- package/dist/dashboard/.next/server/pages/404.html +1 -1
- package/dist/dashboard/.next/server/pages/500.html +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +233 -60
- package/dist/index.js +262 -32
- package/dist/migrate/import-orchestration.d.ts +31 -0
- package/dist/migrate/import-orchestration.js +638 -0
- package/dist/onboard.js +130 -35
- package/dist/orchestration/evidence.d.ts +7 -0
- package/dist/orchestration/evidence.js +79 -4
- package/dist/orchestration/pipeline.d.ts +1 -0
- package/dist/orchestration/pipeline.js +26 -1
- package/dist/state/tasks.d.ts +37 -2
- package/dist/state/tasks.js +242 -10
- package/dist/state/verify.js +89 -11
- package/package.json +1 -1
- /package/dist/dashboard/.next/static/{iKGx5hWe1zbwJZWchF9kg → EZjkAnODdTglaMXuBw76E}/_buildManifest.js +0 -0
- /package/dist/dashboard/.next/static/{iKGx5hWe1zbwJZWchF9kg → EZjkAnODdTglaMXuBw76E}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadEvents, nowIso } from '../state/events.js';
|
|
4
|
+
import { ensureBaseState } from '../state/paths.js';
|
|
5
|
+
import { rebuildIndex } from '../state/tasks.js';
|
|
6
|
+
import { verifyState } from '../state/verify.js';
|
|
7
|
+
const TASK_ID_RE = /^(DEV|TEST|TASK)-(\d{8})-(\d{3})$/;
|
|
8
|
+
const ACTIVE_STATUSES = new Set(['OPEN', 'IN_PROGRESS', 'BLOCKED', 'DONE_PENDING_REPORT']);
|
|
9
|
+
function readJson(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function readText(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function toNonEmptyString(value) {
|
|
26
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
27
|
+
}
|
|
28
|
+
function toStringArray(value) {
|
|
29
|
+
return Array.isArray(value)
|
|
30
|
+
? value.map((item) => String(item).trim()).filter(Boolean)
|
|
31
|
+
: [];
|
|
32
|
+
}
|
|
33
|
+
function sameStringArray(left, right) {
|
|
34
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
35
|
+
}
|
|
36
|
+
function collectJsonLines(filePath) {
|
|
37
|
+
if (!fs.existsSync(filePath))
|
|
38
|
+
return 0;
|
|
39
|
+
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/).filter(Boolean).length;
|
|
40
|
+
}
|
|
41
|
+
function collectFilesRecursive(baseDir) {
|
|
42
|
+
if (!fs.existsSync(baseDir))
|
|
43
|
+
return [];
|
|
44
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
45
|
+
const files = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
files.push(...collectFilesRecursive(fullPath));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
files.push(fullPath);
|
|
53
|
+
}
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
function resolveImportSource(fromDir) {
|
|
57
|
+
const baseDir = path.resolve(fromDir);
|
|
58
|
+
const tasksDir = path.join(baseDir, 'tasks');
|
|
59
|
+
const evidenceDir = path.join(baseDir, 'evidence');
|
|
60
|
+
const promptsDir = path.join(baseDir, 'prompts');
|
|
61
|
+
const eventsFile = path.join(baseDir, 'tasks.jsonl');
|
|
62
|
+
const indexFile = path.join(baseDir, 'index.json');
|
|
63
|
+
if (!fs.existsSync(baseDir))
|
|
64
|
+
throw new Error(`legacy orchestration dir not found: ${baseDir}`);
|
|
65
|
+
if (!fs.existsSync(tasksDir))
|
|
66
|
+
throw new Error(`legacy tasks dir not found: ${tasksDir}`);
|
|
67
|
+
if (!fs.existsSync(eventsFile))
|
|
68
|
+
throw new Error(`legacy tasks.jsonl not found: ${eventsFile}`);
|
|
69
|
+
return { baseDir, tasksDir, evidenceDir, promptsDir, eventsFile, indexFile };
|
|
70
|
+
}
|
|
71
|
+
function ensureDestinationEmpty(paths) {
|
|
72
|
+
ensureBaseState(paths);
|
|
73
|
+
const taskFiles = fs.readdirSync(paths.tasksDir).filter((name) => /\.(meta\.json|json|md)$/.test(name));
|
|
74
|
+
const evidenceFiles = collectFilesRecursive(paths.evidenceDir);
|
|
75
|
+
const promptFiles = collectFilesRecursive(paths.promptsDir);
|
|
76
|
+
const eventCount = collectJsonLines(paths.eventsFile);
|
|
77
|
+
if (taskFiles.length || evidenceFiles.length || promptFiles.length || eventCount) {
|
|
78
|
+
throw new Error('refusing to import into non-empty runtime state; use a clean zigrix baseDir or run `zigrix reset state --yes` first');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function parseTaskSpecMarkdown(raw) {
|
|
82
|
+
const pick = (patterns) => {
|
|
83
|
+
for (const pattern of patterns) {
|
|
84
|
+
const matched = raw.match(pattern);
|
|
85
|
+
if (matched?.[1])
|
|
86
|
+
return matched[1].trim();
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
const parseAgents = (value) => {
|
|
91
|
+
if (!value)
|
|
92
|
+
return undefined;
|
|
93
|
+
const items = value
|
|
94
|
+
.split(',')
|
|
95
|
+
.map((item) => item.replace(/`/g, '').trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
return items.length > 0 ? items : undefined;
|
|
98
|
+
};
|
|
99
|
+
const inScope = raw.match(/### In-Scope\n([\s\S]*?)(?:\n### Out-of-Scope|\n## )/);
|
|
100
|
+
const description = inScope?.[1]
|
|
101
|
+
?.split(/\r?\n/)
|
|
102
|
+
.map((line) => line.trim())
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(' ');
|
|
105
|
+
return {
|
|
106
|
+
title: pick([/^- Title:\s*(.+)$/m, /^- \*\*Title:\*\*\s*(.+)$/m]),
|
|
107
|
+
requestedBy: pick([/^- Requested by:\s*(.+)$/m]),
|
|
108
|
+
createdAt: pick([/^- Created at \(KST\):\s*(.+)$/m, /^- Created:\s*(.+)$/m]),
|
|
109
|
+
status: pick([/^- Current Status:\s*`?([^`\n]+)`?$/m, /^- Status:\s*`?([^`\n]+)`?$/m]),
|
|
110
|
+
scale: pick([/^- Scale:\s*`?([^`\n]+)`?$/m]),
|
|
111
|
+
description: description && description !== '-' ? description : undefined,
|
|
112
|
+
requiredAgents: parseAgents(pick([/^- Required agents:\s*(.+)$/m, /^- \*\*Required Agents:\*\*\s*(.+)$/m])),
|
|
113
|
+
orchestratorId: pick([/^- Orchestrator:\s*`?([^`\n]+)`?$/m]),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function collectTaskIds(source, events) {
|
|
117
|
+
const supported = new Set();
|
|
118
|
+
const skipped = new Set();
|
|
119
|
+
const consider = (candidate) => {
|
|
120
|
+
if (!candidate)
|
|
121
|
+
return;
|
|
122
|
+
if (TASK_ID_RE.test(candidate)) {
|
|
123
|
+
supported.add(candidate);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
skipped.add(candidate);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (const name of fs.readdirSync(source.tasksDir)) {
|
|
130
|
+
if (name.endsWith('.meta.json'))
|
|
131
|
+
consider(name.replace(/\.meta\.json$/, ''));
|
|
132
|
+
if (name.endsWith('.json') && !name.endsWith('.meta.json'))
|
|
133
|
+
consider(name.replace(/\.json$/, ''));
|
|
134
|
+
if (name.endsWith('.md'))
|
|
135
|
+
consider(name.replace(/\.md$/, ''));
|
|
136
|
+
}
|
|
137
|
+
if (fs.existsSync(source.evidenceDir)) {
|
|
138
|
+
for (const name of fs.readdirSync(source.evidenceDir)) {
|
|
139
|
+
const fullPath = path.join(source.evidenceDir, name);
|
|
140
|
+
if (fs.statSync(fullPath).isDirectory())
|
|
141
|
+
consider(name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const event of events) {
|
|
145
|
+
consider(toNonEmptyString(event.taskId));
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
taskIds: [...supported].sort(),
|
|
149
|
+
skippedTaskIds: [...skipped].sort(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function groupEventsByTaskId(events) {
|
|
153
|
+
const grouped = new Map();
|
|
154
|
+
for (const event of events) {
|
|
155
|
+
const taskId = toNonEmptyString(event.taskId);
|
|
156
|
+
if (!taskId)
|
|
157
|
+
continue;
|
|
158
|
+
grouped.set(taskId, [...(grouped.get(taskId) ?? []), event]);
|
|
159
|
+
}
|
|
160
|
+
return grouped;
|
|
161
|
+
}
|
|
162
|
+
function latestEventStatus(events) {
|
|
163
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
164
|
+
const status = toNonEmptyString(events[index]?.status);
|
|
165
|
+
if (status)
|
|
166
|
+
return status;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function latestEventTimestamp(events) {
|
|
171
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
172
|
+
const ts = toNonEmptyString(events[index]?.ts);
|
|
173
|
+
if (ts)
|
|
174
|
+
return ts;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function statusFromIndex(indexData, taskId) {
|
|
179
|
+
if (!indexData || typeof indexData.statusBuckets !== 'object' || !indexData.statusBuckets || Array.isArray(indexData.statusBuckets)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
for (const [status, rawTaskIds] of Object.entries(indexData.statusBuckets)) {
|
|
183
|
+
if (toStringArray(rawTaskIds).includes(taskId))
|
|
184
|
+
return status;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function collectRequiredAgents(baseMeta, parsed, events, evidenceAgents) {
|
|
189
|
+
const fromMeta = [
|
|
190
|
+
...toStringArray(baseMeta?.requiredAgents),
|
|
191
|
+
...toStringArray(baseMeta?.selectedAgents),
|
|
192
|
+
...toStringArray(baseMeta?.baselineRequiredAgents),
|
|
193
|
+
];
|
|
194
|
+
if (fromMeta.length > 0)
|
|
195
|
+
return [...new Set(fromMeta)].sort();
|
|
196
|
+
if (parsed.requiredAgents?.length)
|
|
197
|
+
return [...new Set(parsed.requiredAgents)].sort();
|
|
198
|
+
const fromEvents = new Set();
|
|
199
|
+
for (const event of events) {
|
|
200
|
+
toStringArray(event.payload?.requiredAgents).forEach((agentId) => fromEvents.add(agentId));
|
|
201
|
+
toStringArray(event.requiredAgents).forEach((agentId) => fromEvents.add(agentId));
|
|
202
|
+
const targetAgent = toNonEmptyString(event.targetAgent);
|
|
203
|
+
if (targetAgent)
|
|
204
|
+
fromEvents.add(targetAgent);
|
|
205
|
+
const payloadAgent = toNonEmptyString(event.payload?.agentId);
|
|
206
|
+
if (payloadAgent)
|
|
207
|
+
fromEvents.add(payloadAgent);
|
|
208
|
+
const topAgent = toNonEmptyString(event.agentId);
|
|
209
|
+
if (topAgent)
|
|
210
|
+
fromEvents.add(topAgent);
|
|
211
|
+
}
|
|
212
|
+
evidenceAgents.forEach((agentId) => fromEvents.add(agentId));
|
|
213
|
+
return [...fromEvents].sort();
|
|
214
|
+
}
|
|
215
|
+
function resolveAgentIdFromEvent(event) {
|
|
216
|
+
return toNonEmptyString(event.targetAgent)
|
|
217
|
+
?? toNonEmptyString(event.agentId)
|
|
218
|
+
?? toNonEmptyString(event.payload?.agentId)
|
|
219
|
+
?? null;
|
|
220
|
+
}
|
|
221
|
+
function collectWorkerSessions(baseMeta, events, evidenceDir) {
|
|
222
|
+
const workerSessions = baseMeta?.workerSessions && typeof baseMeta.workerSessions === 'object' && !Array.isArray(baseMeta.workerSessions)
|
|
223
|
+
? structuredClone(baseMeta.workerSessions)
|
|
224
|
+
: {};
|
|
225
|
+
const updateWorker = (agentId, patch) => {
|
|
226
|
+
const current = workerSessions[agentId] && typeof workerSessions[agentId] === 'object' && !Array.isArray(workerSessions[agentId])
|
|
227
|
+
? workerSessions[agentId]
|
|
228
|
+
: {};
|
|
229
|
+
workerSessions[agentId] = {
|
|
230
|
+
...current,
|
|
231
|
+
...Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== null && value !== undefined && value !== '')),
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
for (const event of events) {
|
|
235
|
+
const eventName = toNonEmptyString(event.event);
|
|
236
|
+
const agentId = resolveAgentIdFromEvent(event);
|
|
237
|
+
if (!agentId)
|
|
238
|
+
continue;
|
|
239
|
+
const payload = event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
|
|
240
|
+
? event.payload
|
|
241
|
+
: {};
|
|
242
|
+
const result = toNonEmptyString(payload.result);
|
|
243
|
+
const status = eventName === 'worker_dispatched'
|
|
244
|
+
? 'dispatched'
|
|
245
|
+
: eventName === 'worker_done'
|
|
246
|
+
? (result ?? 'done')
|
|
247
|
+
: eventName === 'worker_started'
|
|
248
|
+
? 'in_progress'
|
|
249
|
+
: eventName === 'evidence_collected'
|
|
250
|
+
? 'done'
|
|
251
|
+
: null;
|
|
252
|
+
updateWorker(agentId, {
|
|
253
|
+
sessionKey: toNonEmptyString(event.sessionKey),
|
|
254
|
+
sessionId: toNonEmptyString(event.sessionId),
|
|
255
|
+
runId: toNonEmptyString(event.runId),
|
|
256
|
+
unitId: toNonEmptyString(event.unitId),
|
|
257
|
+
workPackage: toNonEmptyString(event.workPackage),
|
|
258
|
+
status,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (fs.existsSync(evidenceDir)) {
|
|
262
|
+
for (const fileName of fs.readdirSync(evidenceDir).filter((name) => name.endsWith('.json') && name !== '_merged.json')) {
|
|
263
|
+
const data = readJson(path.join(evidenceDir, fileName));
|
|
264
|
+
if (!data)
|
|
265
|
+
continue;
|
|
266
|
+
const agentId = toNonEmptyString(data.agentId) ?? fileName.replace(/\.json$/, '');
|
|
267
|
+
if (!agentId)
|
|
268
|
+
continue;
|
|
269
|
+
updateWorker(agentId, {
|
|
270
|
+
sessionKey: toNonEmptyString(data.sessionKey),
|
|
271
|
+
sessionId: toNonEmptyString(data.sessionId),
|
|
272
|
+
runId: toNonEmptyString(data.runId),
|
|
273
|
+
unitId: toNonEmptyString(data.unitId),
|
|
274
|
+
status: 'done',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return workerSessions;
|
|
279
|
+
}
|
|
280
|
+
function detectOrchestrator(baseMeta, parsed, events) {
|
|
281
|
+
const result = {};
|
|
282
|
+
const baseOrchestratorId = toNonEmptyString(baseMeta?.orchestratorId) ?? parsed.orchestratorId ?? null;
|
|
283
|
+
if (baseOrchestratorId)
|
|
284
|
+
result.orchestratorId = baseOrchestratorId;
|
|
285
|
+
const baseSessionKey = toNonEmptyString(baseMeta?.orchestratorSessionKey);
|
|
286
|
+
const baseSessionId = toNonEmptyString(baseMeta?.orchestratorSessionId);
|
|
287
|
+
if (baseSessionKey)
|
|
288
|
+
result.orchestratorSessionKey = baseSessionKey;
|
|
289
|
+
if (baseSessionId)
|
|
290
|
+
result.orchestratorSessionId = baseSessionId;
|
|
291
|
+
for (const event of events) {
|
|
292
|
+
const eventName = toNonEmptyString(event.event);
|
|
293
|
+
if (!eventName)
|
|
294
|
+
continue;
|
|
295
|
+
if (eventName === 'dispatch_started') {
|
|
296
|
+
const orchestrator = event.orchestrator && typeof event.orchestrator === 'object' && !Array.isArray(event.orchestrator)
|
|
297
|
+
? event.orchestrator
|
|
298
|
+
: null;
|
|
299
|
+
const orchestratorId = toNonEmptyString(orchestrator?.agentId) ?? toNonEmptyString(event.actor);
|
|
300
|
+
const sessionKey = toNonEmptyString(orchestrator?.sessionKey) ?? toNonEmptyString(event.sessionKey);
|
|
301
|
+
const sessionId = toNonEmptyString(orchestrator?.sessionId) ?? toNonEmptyString(event.sessionId);
|
|
302
|
+
if (orchestratorId)
|
|
303
|
+
result.orchestratorId = orchestratorId;
|
|
304
|
+
if (sessionKey)
|
|
305
|
+
result.orchestratorSessionKey = sessionKey;
|
|
306
|
+
if (sessionId)
|
|
307
|
+
result.orchestratorSessionId = sessionId;
|
|
308
|
+
}
|
|
309
|
+
if (eventName === 'task_started' && !result.orchestratorSessionKey) {
|
|
310
|
+
const sessionKey = toNonEmptyString(event.sessionKey);
|
|
311
|
+
const sessionId = toNonEmptyString(event.sessionId);
|
|
312
|
+
const actor = toNonEmptyString(event.actor);
|
|
313
|
+
if (actor)
|
|
314
|
+
result.orchestratorId = actor;
|
|
315
|
+
if (sessionKey)
|
|
316
|
+
result.orchestratorSessionKey = sessionKey;
|
|
317
|
+
if (sessionId)
|
|
318
|
+
result.orchestratorSessionId = sessionId;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function detectQaAgentId(requiredAgents) {
|
|
324
|
+
return requiredAgents.find((agentId) => /(^qa[-_]|-qa$|\bqa\b|quality|test)/i.test(agentId));
|
|
325
|
+
}
|
|
326
|
+
function normalizeLegacyTask(params) {
|
|
327
|
+
const { taskId, baseMeta, parsedSpec, events, evidenceDir, sourceIndex } = params;
|
|
328
|
+
const evidenceAgents = fs.existsSync(evidenceDir)
|
|
329
|
+
? fs.readdirSync(evidenceDir).filter((name) => name.endsWith('.json') && name !== '_merged.json').map((name) => name.replace(/\.json$/, '')).sort()
|
|
330
|
+
: [];
|
|
331
|
+
const requiredAgents = collectRequiredAgents(baseMeta, parsedSpec, events, evidenceAgents);
|
|
332
|
+
const workerSessions = collectWorkerSessions(baseMeta, events, evidenceDir);
|
|
333
|
+
const orchestrator = detectOrchestrator(baseMeta, parsedSpec, events);
|
|
334
|
+
const title = toNonEmptyString(baseMeta?.title) ?? parsedSpec.title ?? taskId;
|
|
335
|
+
const description = toNonEmptyString(baseMeta?.description) ?? parsedSpec.description ?? title;
|
|
336
|
+
const scale = toNonEmptyString(baseMeta?.scale) ?? parsedSpec.scale ?? 'normal';
|
|
337
|
+
const createdAt = toNonEmptyString(baseMeta?.createdAt) ?? parsedSpec.createdAt ?? latestEventTimestamp(events) ?? nowIso();
|
|
338
|
+
const updatedAt = toNonEmptyString(baseMeta?.updatedAt) ?? latestEventTimestamp(events) ?? createdAt;
|
|
339
|
+
const status = toNonEmptyString(baseMeta?.status)
|
|
340
|
+
?? latestEventStatus(events)
|
|
341
|
+
?? statusFromIndex(sourceIndex, taskId)
|
|
342
|
+
?? parsedSpec.status
|
|
343
|
+
?? 'OPEN';
|
|
344
|
+
const requestedBy = toNonEmptyString(baseMeta?.requestedBy) ?? parsedSpec.requestedBy ?? undefined;
|
|
345
|
+
const projectDir = toNonEmptyString(baseMeta?.projectDir) ?? toNonEmptyString(baseMeta?.projectPath) ?? undefined;
|
|
346
|
+
const normalized = {
|
|
347
|
+
...(baseMeta ?? {}),
|
|
348
|
+
taskId,
|
|
349
|
+
title,
|
|
350
|
+
description,
|
|
351
|
+
scale,
|
|
352
|
+
status,
|
|
353
|
+
createdAt,
|
|
354
|
+
updatedAt,
|
|
355
|
+
requiredAgents,
|
|
356
|
+
workerSessions,
|
|
357
|
+
...(requestedBy ? { requestedBy } : {}),
|
|
358
|
+
...(projectDir ? { projectDir } : {}),
|
|
359
|
+
...(orchestrator.orchestratorId ? { orchestratorId: orchestrator.orchestratorId } : {}),
|
|
360
|
+
...(orchestrator.orchestratorSessionKey ? { orchestratorSessionKey: orchestrator.orchestratorSessionKey } : {}),
|
|
361
|
+
...(orchestrator.orchestratorSessionId ? { orchestratorSessionId: orchestrator.orchestratorSessionId } : {}),
|
|
362
|
+
...(toNonEmptyString(baseMeta?.qaAgentId) || detectQaAgentId(requiredAgents)
|
|
363
|
+
? { qaAgentId: toNonEmptyString(baseMeta?.qaAgentId) ?? detectQaAgentId(requiredAgents) }
|
|
364
|
+
: {}),
|
|
365
|
+
};
|
|
366
|
+
return normalized;
|
|
367
|
+
}
|
|
368
|
+
function buildLegacyQaVerification(task, presentAgents, existing) {
|
|
369
|
+
const current = existing?.qaVerification && typeof existing.qaVerification === 'object' && !Array.isArray(existing.qaVerification)
|
|
370
|
+
? existing.qaVerification
|
|
371
|
+
: null;
|
|
372
|
+
if (current)
|
|
373
|
+
return current;
|
|
374
|
+
const qaAgentId = toNonEmptyString(existing?.qaAgentId) ?? task.qaAgentId ?? detectQaAgentId(task.requiredAgents) ?? null;
|
|
375
|
+
const qaPresent = qaAgentId ? presentAgents.includes(qaAgentId) : false;
|
|
376
|
+
return {
|
|
377
|
+
required: qaPresent,
|
|
378
|
+
mappingCount: 0,
|
|
379
|
+
mappings: [],
|
|
380
|
+
complete: true,
|
|
381
|
+
importedLegacy: true,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function buildMergedItemsFromEvidence(evidenceDir) {
|
|
385
|
+
if (!fs.existsSync(evidenceDir))
|
|
386
|
+
return [];
|
|
387
|
+
const files = fs.readdirSync(evidenceDir).filter((name) => name.endsWith('.json') && name !== '_merged.json').sort();
|
|
388
|
+
return files.flatMap((fileName) => {
|
|
389
|
+
const data = readJson(path.join(evidenceDir, fileName));
|
|
390
|
+
if (!data)
|
|
391
|
+
return [];
|
|
392
|
+
const agentId = toNonEmptyString(data.agentId) ?? fileName.replace(/\.json$/, '');
|
|
393
|
+
if (!agentId)
|
|
394
|
+
return [];
|
|
395
|
+
return [{
|
|
396
|
+
agentId,
|
|
397
|
+
unitId: data.unitId ?? null,
|
|
398
|
+
runId: data.runId ?? '',
|
|
399
|
+
sessionKey: data.sessionKey ?? null,
|
|
400
|
+
sessionId: data.sessionId ?? null,
|
|
401
|
+
transcriptPath: data.transcriptPath ?? null,
|
|
402
|
+
evidence: data.evidence ?? {},
|
|
403
|
+
}];
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function importTaskEvidence(source, destination, task) {
|
|
407
|
+
const sourceDir = path.join(source.evidenceDir, task.taskId);
|
|
408
|
+
if (!fs.existsSync(sourceDir))
|
|
409
|
+
return;
|
|
410
|
+
const destinationDir = path.join(destination.evidenceDir, task.taskId);
|
|
411
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
412
|
+
for (const filePath of collectFilesRecursive(sourceDir)) {
|
|
413
|
+
const relative = path.relative(sourceDir, filePath);
|
|
414
|
+
if (path.basename(filePath) === '_merged.json')
|
|
415
|
+
continue;
|
|
416
|
+
const targetPath = path.join(destinationDir, relative);
|
|
417
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
418
|
+
fs.copyFileSync(filePath, targetPath);
|
|
419
|
+
}
|
|
420
|
+
const sourceMerged = readJson(path.join(sourceDir, '_merged.json'));
|
|
421
|
+
const items = buildMergedItemsFromEvidence(destinationDir);
|
|
422
|
+
if (!sourceMerged && items.length === 0)
|
|
423
|
+
return;
|
|
424
|
+
const presentAgents = items.map((item) => String(item.agentId)).sort();
|
|
425
|
+
const requiredAgents = toStringArray(sourceMerged?.requiredAgents);
|
|
426
|
+
const normalizedRequiredAgents = requiredAgents.length > 0 ? requiredAgents : task.requiredAgents;
|
|
427
|
+
const qaAgentId = toNonEmptyString(sourceMerged?.qaAgentId) ?? task.qaAgentId ?? detectQaAgentId(normalizedRequiredAgents) ?? null;
|
|
428
|
+
const qaPresent = typeof sourceMerged?.qaPresent === 'boolean'
|
|
429
|
+
? Boolean(sourceMerged.qaPresent)
|
|
430
|
+
: Boolean(qaAgentId && presentAgents.includes(qaAgentId));
|
|
431
|
+
const qaVerification = buildLegacyQaVerification(task, presentAgents, sourceMerged);
|
|
432
|
+
const missingAgents = toStringArray(sourceMerged?.missingAgents).length > 0
|
|
433
|
+
? toStringArray(sourceMerged?.missingAgents)
|
|
434
|
+
: normalizedRequiredAgents.filter((agentId) => !presentAgents.includes(agentId));
|
|
435
|
+
const complete = typeof sourceMerged?.complete === 'boolean'
|
|
436
|
+
? Boolean(sourceMerged.complete)
|
|
437
|
+
: missingAgents.length === 0 && (!(qaVerification.required === true) || qaVerification.complete === true);
|
|
438
|
+
const merged = {
|
|
439
|
+
...(sourceMerged ?? {}),
|
|
440
|
+
ts: toNonEmptyString(sourceMerged?.ts) ?? task.updatedAt,
|
|
441
|
+
taskId: task.taskId,
|
|
442
|
+
requiredAgents: normalizedRequiredAgents,
|
|
443
|
+
presentAgents,
|
|
444
|
+
missingAgents,
|
|
445
|
+
qaAgentId,
|
|
446
|
+
qaPresent,
|
|
447
|
+
qaVerification,
|
|
448
|
+
complete,
|
|
449
|
+
items: sourceMerged?.items && Array.isArray(sourceMerged.items) ? sourceMerged.items : items,
|
|
450
|
+
};
|
|
451
|
+
fs.writeFileSync(path.join(destinationDir, '_merged.json'), `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
|
|
452
|
+
}
|
|
453
|
+
function normalizeImportedEvent(event) {
|
|
454
|
+
const { timestamp: _legacyTimestamp, ...rest } = event;
|
|
455
|
+
return {
|
|
456
|
+
...rest,
|
|
457
|
+
ts: toNonEmptyString(event.ts) ?? nowIso(),
|
|
458
|
+
payload: event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
|
|
459
|
+
? event.payload
|
|
460
|
+
: {},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function writeEventsFile(eventsFile, events) {
|
|
464
|
+
const normalized = events.map((event) => normalizeImportedEvent(event));
|
|
465
|
+
fs.writeFileSync(eventsFile, `${normalized.map((event) => JSON.stringify(event)).join('\n')}\n`, 'utf8');
|
|
466
|
+
}
|
|
467
|
+
function writeTaskFiles(paths, task, sourceMdPath) {
|
|
468
|
+
fs.writeFileSync(path.join(paths.tasksDir, `${task.taskId}.meta.json`), `${JSON.stringify(task, null, 2)}\n`, 'utf8');
|
|
469
|
+
const destinationMd = path.join(paths.tasksDir, `${task.taskId}.md`);
|
|
470
|
+
if (sourceMdPath && fs.existsSync(sourceMdPath)) {
|
|
471
|
+
fs.copyFileSync(sourceMdPath, destinationMd);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
fs.writeFileSync(destinationMd, `# Task Spec\n\n## 0) Task Metadata\n- Task ID: \`${task.taskId}\`\n- Title: ${task.title}\n- Scale: \`${task.scale}\`\n- Status: \`${task.status}\`\n\n## 1) Scope\n${task.description}\n`, 'utf8');
|
|
475
|
+
}
|
|
476
|
+
function copyPrompts(source, destination) {
|
|
477
|
+
if (!fs.existsSync(source.promptsDir))
|
|
478
|
+
return 0;
|
|
479
|
+
const promptFiles = collectFilesRecursive(source.promptsDir);
|
|
480
|
+
for (const filePath of promptFiles) {
|
|
481
|
+
const relative = path.relative(source.promptsDir, filePath);
|
|
482
|
+
const targetPath = path.join(destination.promptsDir, relative);
|
|
483
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
484
|
+
fs.copyFileSync(filePath, targetPath);
|
|
485
|
+
}
|
|
486
|
+
return promptFiles.length;
|
|
487
|
+
}
|
|
488
|
+
function computeSourceCounts(source, taskIds, events) {
|
|
489
|
+
const evidenceDirs = fs.existsSync(source.evidenceDir)
|
|
490
|
+
? fs.readdirSync(source.evidenceDir).filter((name) => fs.statSync(path.join(source.evidenceDir, name)).isDirectory() && taskIds.includes(name)).length
|
|
491
|
+
: 0;
|
|
492
|
+
const evidenceJsonFiles = fs.existsSync(source.evidenceDir)
|
|
493
|
+
? collectFilesRecursive(source.evidenceDir).filter((filePath) => filePath.endsWith('.json'))
|
|
494
|
+
: [];
|
|
495
|
+
const mergedFiles = evidenceJsonFiles.filter((filePath) => path.basename(filePath) === '_merged.json').length;
|
|
496
|
+
const evidenceFiles = evidenceJsonFiles.length - mergedFiles;
|
|
497
|
+
const prompts = fs.existsSync(source.promptsDir) ? collectFilesRecursive(source.promptsDir).length : 0;
|
|
498
|
+
return {
|
|
499
|
+
tasks: taskIds.length,
|
|
500
|
+
evidenceDirs,
|
|
501
|
+
evidenceFiles,
|
|
502
|
+
mergedFiles,
|
|
503
|
+
prompts,
|
|
504
|
+
events: events.length,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function computeImportedCounts(paths) {
|
|
508
|
+
const tasks = fs.readdirSync(paths.tasksDir).filter((name) => name.endsWith('.meta.json')).length;
|
|
509
|
+
const evidenceDirs = fs.existsSync(paths.evidenceDir)
|
|
510
|
+
? fs.readdirSync(paths.evidenceDir).filter((name) => fs.statSync(path.join(paths.evidenceDir, name)).isDirectory()).length
|
|
511
|
+
: 0;
|
|
512
|
+
const evidenceJsonFiles = fs.existsSync(paths.evidenceDir)
|
|
513
|
+
? collectFilesRecursive(paths.evidenceDir).filter((filePath) => filePath.endsWith('.json'))
|
|
514
|
+
: [];
|
|
515
|
+
const mergedFiles = evidenceJsonFiles.filter((filePath) => path.basename(filePath) === '_merged.json').length;
|
|
516
|
+
const evidenceFiles = evidenceJsonFiles.length - mergedFiles;
|
|
517
|
+
const prompts = collectFilesRecursive(paths.promptsDir).length;
|
|
518
|
+
const events = collectJsonLines(paths.eventsFile);
|
|
519
|
+
return { tasks, evidenceDirs, evidenceFiles, mergedFiles, prompts, events };
|
|
520
|
+
}
|
|
521
|
+
function filteredStatusBuckets(indexData, supportedTaskIds) {
|
|
522
|
+
if (!indexData || typeof indexData.statusBuckets !== 'object' || !indexData.statusBuckets || Array.isArray(indexData.statusBuckets)) {
|
|
523
|
+
return {};
|
|
524
|
+
}
|
|
525
|
+
const supported = new Set(supportedTaskIds);
|
|
526
|
+
const buckets = {};
|
|
527
|
+
for (const [status, rawTaskIds] of Object.entries(indexData.statusBuckets)) {
|
|
528
|
+
buckets[status] = toStringArray(rawTaskIds).filter((taskId) => supported.has(taskId)).sort();
|
|
529
|
+
}
|
|
530
|
+
return buckets;
|
|
531
|
+
}
|
|
532
|
+
function compareStatusBuckets(sourceIndex, destIndex, taskIds) {
|
|
533
|
+
if (!sourceIndex)
|
|
534
|
+
return true;
|
|
535
|
+
const sourceBuckets = filteredStatusBuckets(sourceIndex, taskIds);
|
|
536
|
+
const destBuckets = filteredStatusBuckets(destIndex, taskIds);
|
|
537
|
+
const statuses = new Set([...Object.keys(sourceBuckets), ...Object.keys(destBuckets)]);
|
|
538
|
+
for (const status of statuses) {
|
|
539
|
+
if (!sameStringArray(sourceBuckets[status] ?? [], destBuckets[status] ?? []))
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
function compareActiveTasks(sourceIndex, destIndex, taskIds) {
|
|
545
|
+
if (!sourceIndex)
|
|
546
|
+
return true;
|
|
547
|
+
const supported = new Set(taskIds);
|
|
548
|
+
const sourceActive = sourceIndex && typeof sourceIndex.activeTasks === 'object' && sourceIndex.activeTasks && !Array.isArray(sourceIndex.activeTasks)
|
|
549
|
+
? Object.keys(sourceIndex.activeTasks).filter((taskId) => supported.has(taskId)).sort()
|
|
550
|
+
: taskIds.filter((taskId) => ACTIVE_STATUSES.has(statusFromIndex(sourceIndex, taskId) ?? '')).sort();
|
|
551
|
+
const destActive = destIndex && typeof destIndex.activeTasks === 'object' && destIndex.activeTasks && !Array.isArray(destIndex.activeTasks)
|
|
552
|
+
? Object.keys(destIndex.activeTasks).filter((taskId) => supported.has(taskId)).sort()
|
|
553
|
+
: [];
|
|
554
|
+
return sameStringArray(sourceActive, destActive);
|
|
555
|
+
}
|
|
556
|
+
function saveImportReport(paths, report) {
|
|
557
|
+
fs.mkdirSync(paths.runsDir, { recursive: true });
|
|
558
|
+
const stamp = nowIso().replaceAll(':', '').replace(/\.\d+/, '');
|
|
559
|
+
const reportPath = path.join(paths.runsDir, `migration-import-orchestration-${stamp}.json`);
|
|
560
|
+
fs.writeFileSync(reportPath, `${JSON.stringify({ ...report, reportPath }, null, 2)}\n`, 'utf8');
|
|
561
|
+
return reportPath;
|
|
562
|
+
}
|
|
563
|
+
export function importOrchestrationState(paths, params) {
|
|
564
|
+
const source = resolveImportSource(params.fromDir);
|
|
565
|
+
ensureDestinationEmpty(paths);
|
|
566
|
+
ensureBaseState(paths);
|
|
567
|
+
const sourceEvents = loadEvents(source.eventsFile);
|
|
568
|
+
const sourceIndex = readJson(source.indexFile);
|
|
569
|
+
const { taskIds, skippedTaskIds } = collectTaskIds(source, sourceEvents);
|
|
570
|
+
const eventsByTaskId = groupEventsByTaskId(sourceEvents);
|
|
571
|
+
const synthesizedMetaTasks = [];
|
|
572
|
+
const warnings = [];
|
|
573
|
+
for (const skippedTaskId of skippedTaskIds) {
|
|
574
|
+
warnings.push(`unsupported legacy task id skipped from task import: ${skippedTaskId}`);
|
|
575
|
+
}
|
|
576
|
+
for (const taskId of taskIds) {
|
|
577
|
+
const sourceMetaPath = path.join(source.tasksDir, `${taskId}.meta.json`);
|
|
578
|
+
const sourceLegacyJsonPath = path.join(source.tasksDir, `${taskId}.json`);
|
|
579
|
+
const sourceMdPath = path.join(source.tasksDir, `${taskId}.md`);
|
|
580
|
+
const baseMeta = readJson(sourceMetaPath) ?? readJson(sourceLegacyJsonPath);
|
|
581
|
+
if (!baseMeta)
|
|
582
|
+
synthesizedMetaTasks.push(taskId);
|
|
583
|
+
const parsedSpec = fs.existsSync(sourceMdPath)
|
|
584
|
+
? parseTaskSpecMarkdown(readText(sourceMdPath) ?? '')
|
|
585
|
+
: {};
|
|
586
|
+
const evidenceDir = path.join(source.evidenceDir, taskId);
|
|
587
|
+
const normalizedTask = normalizeLegacyTask({
|
|
588
|
+
taskId,
|
|
589
|
+
baseMeta,
|
|
590
|
+
parsedSpec,
|
|
591
|
+
events: eventsByTaskId.get(taskId) ?? [],
|
|
592
|
+
evidenceDir,
|
|
593
|
+
sourceIndex,
|
|
594
|
+
});
|
|
595
|
+
writeTaskFiles(paths, normalizedTask, fs.existsSync(sourceMdPath) ? sourceMdPath : null);
|
|
596
|
+
importTaskEvidence(source, paths, normalizedTask);
|
|
597
|
+
}
|
|
598
|
+
const promptCount = copyPrompts(source, paths);
|
|
599
|
+
writeEventsFile(paths.eventsFile, sourceEvents);
|
|
600
|
+
const rebuiltIndex = rebuildIndex(paths);
|
|
601
|
+
const stateCheck = verifyState(paths);
|
|
602
|
+
const sourceCounts = computeSourceCounts(source, taskIds, sourceEvents);
|
|
603
|
+
const importedCounts = computeImportedCounts(paths);
|
|
604
|
+
const parity = {
|
|
605
|
+
tasks: sourceCounts.tasks === importedCounts.tasks,
|
|
606
|
+
evidenceDirs: sourceCounts.evidenceDirs === importedCounts.evidenceDirs,
|
|
607
|
+
evidenceFiles: sourceCounts.evidenceFiles === importedCounts.evidenceFiles,
|
|
608
|
+
mergedFiles: importedCounts.mergedFiles >= sourceCounts.mergedFiles,
|
|
609
|
+
prompts: sourceCounts.prompts === importedCounts.prompts,
|
|
610
|
+
events: sourceCounts.events === importedCounts.events,
|
|
611
|
+
statusBuckets: compareStatusBuckets(sourceIndex, rebuiltIndex, taskIds),
|
|
612
|
+
activeTasks: compareActiveTasks(sourceIndex, rebuiltIndex, taskIds),
|
|
613
|
+
};
|
|
614
|
+
if (promptCount !== sourceCounts.prompts) {
|
|
615
|
+
warnings.push(`prompt copy count mismatch: expected ${sourceCounts.prompts}, got ${promptCount}`);
|
|
616
|
+
}
|
|
617
|
+
if (importedCounts.mergedFiles !== sourceCounts.mergedFiles) {
|
|
618
|
+
warnings.push(`merged evidence file count changed during import: source=${sourceCounts.mergedFiles}, imported=${importedCounts.mergedFiles}`);
|
|
619
|
+
}
|
|
620
|
+
const reportWithoutPath = {
|
|
621
|
+
ok: Object.values(parity).every(Boolean) && stateCheck.ok === true,
|
|
622
|
+
action: 'migrate.import-orchestration',
|
|
623
|
+
fromDir: source.baseDir,
|
|
624
|
+
destinationBaseDir: paths.baseDir,
|
|
625
|
+
importedTaskIds: taskIds,
|
|
626
|
+
synthesizedMetaTasks,
|
|
627
|
+
skippedTaskIds,
|
|
628
|
+
counts: {
|
|
629
|
+
source: sourceCounts,
|
|
630
|
+
imported: importedCounts,
|
|
631
|
+
},
|
|
632
|
+
parity,
|
|
633
|
+
stateCheck,
|
|
634
|
+
warnings,
|
|
635
|
+
};
|
|
636
|
+
const reportPath = saveImportReport(paths, reportWithoutPath);
|
|
637
|
+
return { ...reportWithoutPath, reportPath };
|
|
638
|
+
}
|