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
package/dist/state/tasks.js
CHANGED
|
@@ -189,15 +189,226 @@ export function recordTaskProgress(paths, params) {
|
|
|
189
189
|
rebuildIndex(paths);
|
|
190
190
|
return event;
|
|
191
191
|
}
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
function toNonEmptyString(value) {
|
|
193
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
194
|
+
}
|
|
195
|
+
function parseAgentSubagentSessionKey(sessionKey) {
|
|
196
|
+
const matched = sessionKey.match(/^agent:([^:]+):subagent:([^:\s]+)$/);
|
|
197
|
+
if (!matched)
|
|
198
|
+
return null;
|
|
199
|
+
return { agentId: matched[1], sessionId: matched[2] };
|
|
200
|
+
}
|
|
201
|
+
function resolveSessionIdFromSessionsJson(agentsStateDir, agentId, sessionKey) {
|
|
202
|
+
try {
|
|
203
|
+
const sessionsJsonPath = path.join(agentsStateDir, agentId, 'sessions', 'sessions.json');
|
|
204
|
+
const raw = JSON.parse(fs.readFileSync(sessionsJsonPath, 'utf8'));
|
|
205
|
+
const entry = raw[sessionKey];
|
|
206
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
207
|
+
return null;
|
|
208
|
+
return toNonEmptyString(entry.sessionId);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function listDeletedSessionPaths(agentsStateDir, agentId, sessionId) {
|
|
215
|
+
const sessionsDir = path.join(agentsStateDir, agentId, 'sessions');
|
|
216
|
+
if (!fs.existsSync(sessionsDir))
|
|
217
|
+
return [];
|
|
218
|
+
const prefix = `${sessionId}.jsonl.deleted.`;
|
|
219
|
+
try {
|
|
220
|
+
return fs.readdirSync(sessionsDir)
|
|
221
|
+
.filter((name) => name.startsWith(prefix))
|
|
222
|
+
.sort((a, b) => b.localeCompare(a))
|
|
223
|
+
.map((name) => path.join(sessionsDir, name));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function collectTaskSessionRefs(task) {
|
|
230
|
+
const refs = [];
|
|
231
|
+
if (task.orchestratorSessionKey && task.orchestratorId) {
|
|
232
|
+
refs.push({
|
|
233
|
+
scope: 'orchestrator',
|
|
234
|
+
agentId: task.orchestratorId,
|
|
235
|
+
sessionKey: task.orchestratorSessionKey,
|
|
236
|
+
sessionId: task.orchestratorSessionId ?? null,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const [agentId, raw] of Object.entries(task.workerSessions ?? {})) {
|
|
240
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
241
|
+
continue;
|
|
242
|
+
const data = raw;
|
|
243
|
+
const sessionKey = toNonEmptyString(data.sessionKey);
|
|
244
|
+
if (!sessionKey)
|
|
245
|
+
continue;
|
|
246
|
+
refs.push({
|
|
247
|
+
scope: 'worker',
|
|
248
|
+
agentId,
|
|
249
|
+
sessionKey,
|
|
250
|
+
sessionId: toNonEmptyString(data.sessionId),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return refs;
|
|
254
|
+
}
|
|
255
|
+
function diagnoseTaskSessions(task, agentsStateDir) {
|
|
256
|
+
if (!agentsStateDir)
|
|
257
|
+
return [];
|
|
258
|
+
return collectTaskSessionRefs(task).map((ref) => {
|
|
259
|
+
const parsed = parseAgentSubagentSessionKey(ref.sessionKey);
|
|
260
|
+
const agentId = parsed?.agentId ?? ref.agentId;
|
|
261
|
+
let resolvedSessionId = ref.sessionId;
|
|
262
|
+
let mappingSource = ref.sessionId ? 'explicit' : 'none';
|
|
263
|
+
if (!resolvedSessionId && parsed) {
|
|
264
|
+
resolvedSessionId = parsed.sessionId;
|
|
265
|
+
mappingSource = 'parsed';
|
|
266
|
+
}
|
|
267
|
+
if (!resolvedSessionId) {
|
|
268
|
+
const mappedSessionId = resolveSessionIdFromSessionsJson(agentsStateDir, agentId, ref.sessionKey);
|
|
269
|
+
if (mappedSessionId) {
|
|
270
|
+
resolvedSessionId = mappedSessionId;
|
|
271
|
+
mappingSource = 'sessions_json';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!resolvedSessionId) {
|
|
275
|
+
return {
|
|
276
|
+
scope: ref.scope,
|
|
277
|
+
agentId,
|
|
278
|
+
sessionKey: ref.sessionKey,
|
|
279
|
+
sessionId: null,
|
|
280
|
+
mappingSource,
|
|
281
|
+
state: 'missing',
|
|
282
|
+
reason: 'missing_session_mapping',
|
|
283
|
+
activePath: null,
|
|
284
|
+
deletedPath: null,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const activePath = path.join(agentsStateDir, agentId, 'sessions', `${resolvedSessionId}.jsonl`);
|
|
288
|
+
if (fs.existsSync(activePath)) {
|
|
289
|
+
return {
|
|
290
|
+
scope: ref.scope,
|
|
291
|
+
agentId,
|
|
292
|
+
sessionKey: ref.sessionKey,
|
|
293
|
+
sessionId: resolvedSessionId,
|
|
294
|
+
mappingSource,
|
|
295
|
+
state: 'active',
|
|
296
|
+
reason: null,
|
|
297
|
+
activePath,
|
|
298
|
+
deletedPath: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const deletedPaths = listDeletedSessionPaths(agentsStateDir, agentId, resolvedSessionId);
|
|
302
|
+
if (deletedPaths.length > 0) {
|
|
303
|
+
return {
|
|
304
|
+
scope: ref.scope,
|
|
305
|
+
agentId,
|
|
306
|
+
sessionKey: ref.sessionKey,
|
|
307
|
+
sessionId: resolvedSessionId,
|
|
308
|
+
mappingSource,
|
|
309
|
+
state: 'deleted',
|
|
310
|
+
reason: 'session_dead',
|
|
311
|
+
activePath: null,
|
|
312
|
+
deletedPath: deletedPaths[0],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
scope: ref.scope,
|
|
317
|
+
agentId,
|
|
318
|
+
sessionKey: ref.sessionKey,
|
|
319
|
+
sessionId: resolvedSessionId,
|
|
320
|
+
mappingSource,
|
|
321
|
+
state: 'missing',
|
|
322
|
+
reason: null,
|
|
323
|
+
activePath: null,
|
|
324
|
+
deletedPath: null,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function joinUniqueAgentIds(diagnoses, reason) {
|
|
329
|
+
const agentIds = Array.from(new Set(diagnoses.filter((item) => item.reason === reason).map((item) => item.agentId)));
|
|
330
|
+
return agentIds.join(', ') || 'unknown-agent';
|
|
331
|
+
}
|
|
332
|
+
function buildStaleGuidance(task, reasonCode, hours, diagnoses) {
|
|
333
|
+
if (reasonCode === 'session_dead') {
|
|
334
|
+
const agents = joinUniqueAgentIds(diagnoses, 'session_dead');
|
|
335
|
+
return {
|
|
336
|
+
reason: 'session_dead',
|
|
337
|
+
nextAction: `respawn the deleted OpenClaw session for ${agents} and re-register it before resuming ${task.taskId}`,
|
|
338
|
+
resumeHint: `start a fresh session for ${agents}, then update zigrix with the new session key/sessionId before continuing the blocked task`,
|
|
339
|
+
reportLine: `${task.taskId}: BLOCKED session_dead (${agents})`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (reasonCode === 'missing_session_mapping') {
|
|
343
|
+
const agents = joinUniqueAgentIds(diagnoses, 'missing_session_mapping');
|
|
344
|
+
return {
|
|
345
|
+
reason: 'missing_session_mapping',
|
|
346
|
+
nextAction: `repair or re-register the missing session mapping for ${agents} before resuming ${task.taskId}`,
|
|
347
|
+
resumeHint: `re-run worker/orchestrator registration with --session-key and --session-id so zigrix can resolve the backing OpenClaw session`,
|
|
348
|
+
reportLine: `${task.taskId}: BLOCKED missing_session_mapping (${agents})`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
reason: 'stale_timeout',
|
|
353
|
+
nextAction: `inspect the latest progress for ${task.taskId} and either report, refresh progress, or respawn the worker after ${hours}h of inactivity`,
|
|
354
|
+
resumeHint: 'check task status/events/evidence, then continue with a fresh worker registration if the original session is no longer active',
|
|
355
|
+
reportLine: `${task.taskId}: BLOCKED stale_timeout (> ${hours}h)`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function buildStaleTaskSummary(task, hours, agentsStateDir, fallbackReason = 'stale_timeout') {
|
|
194
359
|
const cutoff = Date.now() - hours * 3600 * 1000;
|
|
195
|
-
|
|
360
|
+
const timedOut = Date.parse(task.updatedAt) < cutoff;
|
|
361
|
+
const diagnoses = diagnoseTaskSessions(task, agentsStateDir);
|
|
362
|
+
const hasDeletedSession = diagnoses.some((item) => item.reason === 'session_dead');
|
|
363
|
+
const hasMissingMapping = diagnoses.some((item) => item.reason === 'missing_session_mapping');
|
|
364
|
+
if (!hasDeletedSession && !timedOut)
|
|
365
|
+
return null;
|
|
366
|
+
const reasons = new Set();
|
|
367
|
+
if (timedOut)
|
|
368
|
+
reasons.add('stale_timeout');
|
|
369
|
+
if (hasDeletedSession)
|
|
370
|
+
reasons.add('session_dead');
|
|
371
|
+
if (hasMissingMapping)
|
|
372
|
+
reasons.add('missing_session_mapping');
|
|
373
|
+
const reasonCode = hasDeletedSession
|
|
374
|
+
? 'session_dead'
|
|
375
|
+
: hasMissingMapping && timedOut
|
|
376
|
+
? 'missing_session_mapping'
|
|
377
|
+
: 'stale_timeout';
|
|
378
|
+
const guidance = buildStaleGuidance(task, reasonCode, hours, diagnoses);
|
|
379
|
+
const reason = reasonCode === 'stale_timeout' ? fallbackReason : guidance.reason;
|
|
380
|
+
return {
|
|
381
|
+
taskId: task.taskId,
|
|
382
|
+
title: task.title,
|
|
383
|
+
updatedAt: task.updatedAt,
|
|
384
|
+
hoursThreshold: hours,
|
|
385
|
+
timedOut,
|
|
386
|
+
reason,
|
|
387
|
+
reasonCode,
|
|
388
|
+
reasons: Array.from(reasons),
|
|
389
|
+
nextAction: guidance.nextAction,
|
|
390
|
+
resumeHint: guidance.resumeHint,
|
|
391
|
+
reportLine: guidance.reportLine,
|
|
392
|
+
sessions: diagnoses,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
export function findStaleTasks(paths, hours = 24, options = {}) {
|
|
396
|
+
return listTasks(paths)
|
|
397
|
+
.filter((task) => task.status === 'IN_PROGRESS')
|
|
398
|
+
.map((task) => buildStaleTaskSummary(task, hours, options.agentsStateDir, options.fallbackReason ?? 'stale_timeout'))
|
|
399
|
+
.filter((task) => task !== null);
|
|
196
400
|
}
|
|
197
|
-
export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout') {
|
|
198
|
-
const staleTasks = findStaleTasks(paths, hours);
|
|
199
|
-
const changed = staleTasks.map((
|
|
401
|
+
export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout', options = {}) {
|
|
402
|
+
const staleTasks = findStaleTasks(paths, hours, { ...options, fallbackReason: reason });
|
|
403
|
+
const changed = staleTasks.map((summary) => {
|
|
404
|
+
const task = loadTask(paths, summary.taskId);
|
|
405
|
+
if (!task)
|
|
406
|
+
return null;
|
|
200
407
|
task.status = 'BLOCKED';
|
|
408
|
+
task.nextAction = summary.nextAction;
|
|
409
|
+
task.resumeHint = summary.resumeHint;
|
|
410
|
+
task.staleReason = summary.reasonCode;
|
|
411
|
+
task.staleReasons = summary.reasons;
|
|
201
412
|
saveTask(paths, task);
|
|
202
413
|
const event = appendEvent(paths.eventsFile, {
|
|
203
414
|
event: 'task_blocked',
|
|
@@ -205,12 +416,33 @@ export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout') {
|
|
|
205
416
|
phase: 'recovery',
|
|
206
417
|
actor: 'zigrix',
|
|
207
418
|
status: 'BLOCKED',
|
|
208
|
-
payload: {
|
|
419
|
+
payload: {
|
|
420
|
+
reason: summary.reason,
|
|
421
|
+
reasonCode: summary.reasonCode,
|
|
422
|
+
reasons: summary.reasons,
|
|
423
|
+
previousStatus: 'IN_PROGRESS',
|
|
424
|
+
hoursThreshold: hours,
|
|
425
|
+
timedOut: summary.timedOut,
|
|
426
|
+
nextAction: summary.nextAction,
|
|
427
|
+
resumeHint: summary.resumeHint,
|
|
428
|
+
reportLine: summary.reportLine,
|
|
429
|
+
sessions: summary.sessions,
|
|
430
|
+
},
|
|
209
431
|
});
|
|
210
|
-
return {
|
|
211
|
-
|
|
432
|
+
return {
|
|
433
|
+
taskId: task.taskId,
|
|
434
|
+
reason: summary.reason,
|
|
435
|
+
reasonCode: summary.reasonCode,
|
|
436
|
+
reasons: summary.reasons,
|
|
437
|
+
nextAction: summary.nextAction,
|
|
438
|
+
resumeHint: summary.resumeHint,
|
|
439
|
+
reportLine: summary.reportLine,
|
|
440
|
+
sessions: summary.sessions,
|
|
441
|
+
event,
|
|
442
|
+
};
|
|
443
|
+
}).filter((item) => item !== null);
|
|
212
444
|
rebuildIndex(paths);
|
|
213
|
-
return { ok: true, hours, reason, count: changed.length, changed };
|
|
445
|
+
return { ok: true, hours, requestedReason: reason, count: changed.length, changed };
|
|
214
446
|
}
|
|
215
447
|
// ─── Index ──────────────────────────────────────────────────────────────────
|
|
216
448
|
export function rebuildIndex(paths) {
|
package/dist/state/verify.js
CHANGED
|
@@ -9,6 +9,12 @@ function safeReadJson(filePath) {
|
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
function sortedStrings(value) {
|
|
13
|
+
return Array.isArray(value) ? value.map(String).sort() : [];
|
|
14
|
+
}
|
|
15
|
+
function sameStringArray(left, right) {
|
|
16
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
17
|
+
}
|
|
12
18
|
function verifyTask(paths, task) {
|
|
13
19
|
const issues = [];
|
|
14
20
|
const evidenceDir = path.join(paths.evidenceDir, task.taskId);
|
|
@@ -17,34 +23,58 @@ function verifyTask(paths, task) {
|
|
|
17
23
|
const presentEvidenceAgents = fs.existsSync(evidenceDir)
|
|
18
24
|
? fs.readdirSync(evidenceDir).filter((name) => name.endsWith('.json') && name !== '_merged.json').map((name) => path.basename(name, '.json')).sort()
|
|
19
25
|
: [];
|
|
20
|
-
const requiredAgents = Array.isArray(task.requiredAgents) ? task.requiredAgents.map(String) : [];
|
|
26
|
+
const requiredAgents = Array.isArray(task.requiredAgents) ? task.requiredAgents.map(String).sort() : [];
|
|
21
27
|
const workerAgents = Object.keys(task.workerSessions ?? {}).sort();
|
|
28
|
+
const requiredRoles = Array.isArray(task.requiredRoles) ? task.requiredRoles.map(String).sort() : [];
|
|
29
|
+
const roleAgentMap = task.roleAgentMap && typeof task.roleAgentMap === 'object'
|
|
30
|
+
? task.roleAgentMap
|
|
31
|
+
: {};
|
|
22
32
|
for (const agentId of requiredAgents) {
|
|
23
33
|
if (!workerAgents.includes(agentId) && !presentEvidenceAgents.includes(agentId)) {
|
|
24
34
|
issues.push(`required agent '${agentId}' has neither worker session nor evidence`);
|
|
25
35
|
}
|
|
26
36
|
}
|
|
37
|
+
for (const role of requiredRoles) {
|
|
38
|
+
const mappedAgents = Array.isArray(roleAgentMap[role]) ? roleAgentMap[role].map(String).filter(Boolean) : [];
|
|
39
|
+
if (mappedAgents.length === 0) {
|
|
40
|
+
issues.push(`required role '${role}' has no mapped agent in roleAgentMap`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const hasActiveArtifact = mappedAgents.some((agentId) => requiredAgents.includes(agentId) || workerAgents.includes(agentId) || presentEvidenceAgents.includes(agentId));
|
|
44
|
+
if (!hasActiveArtifact) {
|
|
45
|
+
issues.push(`required role '${role}' has no required agent, worker session, or evidence`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
27
48
|
if (merged) {
|
|
28
|
-
const mergedPresent =
|
|
29
|
-
const mergedMissing =
|
|
49
|
+
const mergedPresent = sortedStrings(merged.presentAgents);
|
|
50
|
+
const mergedMissing = sortedStrings(merged.missingAgents);
|
|
30
51
|
const computedMissing = requiredAgents.filter((agentId) => !presentEvidenceAgents.includes(agentId)).sort();
|
|
31
|
-
if (
|
|
52
|
+
if (!sameStringArray(mergedPresent, presentEvidenceAgents)) {
|
|
32
53
|
issues.push('merged presentAgents does not match evidence files');
|
|
33
54
|
}
|
|
34
|
-
if (
|
|
55
|
+
if (!sameStringArray(mergedMissing, computedMissing)) {
|
|
35
56
|
issues.push('merged missingAgents does not match requiredAgents/evidence files');
|
|
36
57
|
}
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
const qaVerification = merged.qaVerification;
|
|
59
|
+
const qaVerificationComplete = Boolean(qaVerification &&
|
|
60
|
+
typeof qaVerification === 'object' &&
|
|
61
|
+
!Array.isArray(qaVerification) &&
|
|
62
|
+
qaVerification.complete === true);
|
|
63
|
+
if (['DONE_PENDING_REPORT', 'REPORTED'].includes(task.status) && merged.complete !== true) {
|
|
64
|
+
issues.push(`task is ${task.status} but merged evidence is incomplete`);
|
|
65
|
+
}
|
|
66
|
+
if (['DONE_PENDING_REPORT', 'REPORTED'].includes(task.status) && merged.qaPresent === true && !qaVerificationComplete) {
|
|
67
|
+
issues.push('task reached reporting state without DoD↔test verification mapping for QA evidence');
|
|
39
68
|
}
|
|
40
69
|
}
|
|
41
|
-
else if (
|
|
42
|
-
issues.push(
|
|
70
|
+
else if (['DONE_PENDING_REPORT', 'REPORTED'].includes(task.status)) {
|
|
71
|
+
issues.push(`task is ${task.status} but merged evidence is missing`);
|
|
43
72
|
}
|
|
44
73
|
return {
|
|
45
74
|
taskId: task.taskId,
|
|
46
75
|
status: task.status,
|
|
47
76
|
requiredAgents,
|
|
77
|
+
requiredRoles,
|
|
48
78
|
workerAgents,
|
|
49
79
|
presentEvidenceAgents,
|
|
50
80
|
mergedExists: Boolean(merged),
|
|
@@ -52,14 +82,62 @@ function verifyTask(paths, task) {
|
|
|
52
82
|
issues,
|
|
53
83
|
};
|
|
54
84
|
}
|
|
85
|
+
function verifyIndex(paths, tasks) {
|
|
86
|
+
const issues = [];
|
|
87
|
+
const index = safeReadJson(paths.indexFile);
|
|
88
|
+
if (!index) {
|
|
89
|
+
issues.push('index.json is missing or unreadable');
|
|
90
|
+
return { ok: false, issues };
|
|
91
|
+
}
|
|
92
|
+
const counts = index.counts && typeof index.counts === 'object' ? index.counts : {};
|
|
93
|
+
const statusBuckets = index.statusBuckets && typeof index.statusBuckets === 'object'
|
|
94
|
+
? index.statusBuckets
|
|
95
|
+
: {};
|
|
96
|
+
const taskSummaries = index.taskSummaries && typeof index.taskSummaries === 'object'
|
|
97
|
+
? index.taskSummaries
|
|
98
|
+
: {};
|
|
99
|
+
const activeTasks = index.activeTasks && typeof index.activeTasks === 'object'
|
|
100
|
+
? index.activeTasks
|
|
101
|
+
: {};
|
|
102
|
+
if (Number(counts.tasks ?? -1) !== tasks.length) {
|
|
103
|
+
issues.push(`index counts.tasks mismatch: expected ${tasks.length}, got ${String(counts.tasks ?? 'missing')}`);
|
|
104
|
+
}
|
|
105
|
+
const expectedBuckets = new Map();
|
|
106
|
+
for (const task of tasks) {
|
|
107
|
+
expectedBuckets.set(task.status, [...(expectedBuckets.get(task.status) ?? []), task.taskId]);
|
|
108
|
+
const summary = taskSummaries[task.taskId];
|
|
109
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
|
|
110
|
+
issues.push(`taskSummaries missing entry for ${task.taskId}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (summary.status !== task.status) {
|
|
114
|
+
issues.push(`taskSummaries status mismatch for ${task.taskId}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [status, taskIds] of expectedBuckets.entries()) {
|
|
118
|
+
const indexed = sortedStrings(statusBuckets[status]);
|
|
119
|
+
if (!sameStringArray(indexed, taskIds)) {
|
|
120
|
+
issues.push(`statusBuckets mismatch for ${status}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const expectedActiveStatuses = new Set(['OPEN', 'IN_PROGRESS', 'BLOCKED', 'DONE_PENDING_REPORT']);
|
|
124
|
+
const expectedActiveTaskIds = tasks.filter((task) => expectedActiveStatuses.has(task.status)).map((task) => task.taskId).sort();
|
|
125
|
+
const indexedActiveTaskIds = Object.keys(activeTasks).sort();
|
|
126
|
+
if (!sameStringArray(indexedActiveTaskIds, expectedActiveTaskIds)) {
|
|
127
|
+
issues.push('activeTasks mismatch with task statuses');
|
|
128
|
+
}
|
|
129
|
+
return { ok: issues.length === 0, issues };
|
|
130
|
+
}
|
|
55
131
|
export function verifyState(paths) {
|
|
56
132
|
const tasks = listTasks(paths);
|
|
57
133
|
const checks = tasks.map((task) => verifyTask(paths, task));
|
|
58
134
|
const failures = checks.filter((item) => item.ok === false);
|
|
135
|
+
const indexCheck = verifyIndex(paths, tasks);
|
|
59
136
|
return {
|
|
60
|
-
ok: failures.length === 0,
|
|
137
|
+
ok: failures.length === 0 && indexCheck.ok === true,
|
|
61
138
|
taskCount: tasks.length,
|
|
62
|
-
failedCount: failures.length,
|
|
139
|
+
failedCount: failures.length + (indexCheck.ok === true ? 0 : 1),
|
|
63
140
|
checks,
|
|
141
|
+
index: indexCheck,
|
|
64
142
|
};
|
|
65
143
|
}
|
package/package.json
CHANGED
|
File without changes
|
/package/dist/dashboard/.next/static/{iKGx5hWe1zbwJZWchF9kg → EZjkAnODdTglaMXuBw76E}/_ssgManifest.js
RENAMED
|
File without changes
|