zigrix 0.1.1 → 0.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/README.md +10 -2
- 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 +28 -28
- package/dist/dashboard/.next/app-path-routes-manifest.json +3 -3
- package/dist/dashboard/.next/build-manifest.json +5 -5
- 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 +3 -3
- package/dist/dashboard/.next/server/chunks/331.js +1 -1
- package/dist/dashboard/.next/server/functions-config-manifest.json +3 -3
- package/dist/dashboard/.next/server/middleware-build-manifest.js +1 -1
- package/dist/dashboard/.next/server/middleware.js +1 -1
- package/dist/dashboard/.next/server/pages/404.html +1 -1
- package/dist/dashboard/.next/server/pages/500.html +1 -1
- package/dist/dashboard/.next/server/pages/_error.js +1 -1
- package/dist/dashboard/.next/static/chunks/{255-ebd51be49873d76c.js → 255-4f212684648fcab9.js} +1 -1
- package/dist/dashboard/.next/static/chunks/main-cec07dc17fdd452c.js +1 -0
- package/dist/dashboard/package.json +2 -2
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +233 -60
- package/dist/index.js +286 -33
- package/dist/migrate/import-orchestration.d.ts +31 -0
- package/dist/migrate/import-orchestration.js +638 -0
- package/dist/onboard.d.ts +1 -1
- package/dist/onboard.js +132 -37
- package/dist/orchestration/dispatch.js +16 -71
- 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/orchestration/prompt-compose.d.ts +35 -0
- package/dist/orchestration/prompt-compose.js +172 -0
- package/dist/orchestration/worker.d.ts +4 -1
- package/dist/orchestration/worker.js +132 -52
- package/dist/state/tasks.d.ts +43 -2
- package/dist/state/tasks.js +281 -10
- package/dist/state/verify.js +89 -11
- package/package.json +3 -3
- package/rules/defaults/orchestrator-agent.md +4 -2
- package/rules/defaults/worker-common.md +3 -0
- package/skills/oz/SKILL.md +117 -0
- package/skills/oz/references/examples.md +44 -0
- package/skills/oz/references/routing-rubric.md +71 -0
- package/skills/zigrix-main-agent-guide/SKILL.md +37 -7
- package/dist/dashboard/.next/static/chunks/main-da2d845a416cfa3f.js +0 -1
- /package/dist/dashboard/.next/static/{iKGx5hWe1zbwJZWchF9kg → dOjvoQUj-mqwJ8kKG4peU}/_buildManifest.js +0 -0
- /package/dist/dashboard/.next/static/{iKGx5hWe1zbwJZWchF9kg → dOjvoQUj-mqwJ8kKG4peU}/_ssgManifest.js +0 -0
package/dist/state/tasks.js
CHANGED
|
@@ -170,6 +170,45 @@ export function updateTaskStatus(paths, taskId, status) {
|
|
|
170
170
|
rebuildIndex(paths);
|
|
171
171
|
return task;
|
|
172
172
|
}
|
|
173
|
+
export function bindOrchestratorSession(paths, params) {
|
|
174
|
+
const task = loadTask(paths, params.taskId);
|
|
175
|
+
if (!task)
|
|
176
|
+
return null;
|
|
177
|
+
const parsed = parseAgentSubagentSessionKey(params.sessionKey);
|
|
178
|
+
if (parsed && parsed.agentId !== params.agentId) {
|
|
179
|
+
throw new Error(`orchestrator bind validation failed: sessionKey belongs to '${parsed.agentId}', expected '${params.agentId}'`);
|
|
180
|
+
}
|
|
181
|
+
const resolvedSessionId = params.sessionId ?? parsed?.sessionId ?? null;
|
|
182
|
+
task.orchestratorId = params.agentId;
|
|
183
|
+
task.orchestratorSessionKey = params.sessionKey;
|
|
184
|
+
if (resolvedSessionId) {
|
|
185
|
+
task.orchestratorSessionId = resolvedSessionId;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
delete task.orchestratorSessionId;
|
|
189
|
+
}
|
|
190
|
+
saveTask(paths, task);
|
|
191
|
+
appendEvent(paths.eventsFile, {
|
|
192
|
+
event: 'orchestrator_bound',
|
|
193
|
+
taskId: params.taskId,
|
|
194
|
+
phase: 'dispatch',
|
|
195
|
+
actor: 'zigrix',
|
|
196
|
+
targetAgent: params.agentId,
|
|
197
|
+
status: task.status,
|
|
198
|
+
sessionKey: params.sessionKey,
|
|
199
|
+
sessionId: resolvedSessionId,
|
|
200
|
+
payload: { agentId: params.agentId },
|
|
201
|
+
});
|
|
202
|
+
rebuildIndex(paths);
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
taskId: params.taskId,
|
|
206
|
+
agentId: params.agentId,
|
|
207
|
+
sessionKey: params.sessionKey,
|
|
208
|
+
sessionId: resolvedSessionId,
|
|
209
|
+
status: task.status,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
173
212
|
export function recordTaskProgress(paths, params) {
|
|
174
213
|
const task = loadTask(paths, params.taskId);
|
|
175
214
|
if (!task)
|
|
@@ -189,15 +228,226 @@ export function recordTaskProgress(paths, params) {
|
|
|
189
228
|
rebuildIndex(paths);
|
|
190
229
|
return event;
|
|
191
230
|
}
|
|
192
|
-
|
|
193
|
-
|
|
231
|
+
function toNonEmptyString(value) {
|
|
232
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
233
|
+
}
|
|
234
|
+
function parseAgentSubagentSessionKey(sessionKey) {
|
|
235
|
+
const matched = sessionKey.match(/^agent:([^:]+):subagent:([^:\s]+)$/);
|
|
236
|
+
if (!matched)
|
|
237
|
+
return null;
|
|
238
|
+
return { agentId: matched[1], sessionId: matched[2] };
|
|
239
|
+
}
|
|
240
|
+
function resolveSessionIdFromSessionsJson(agentsStateDir, agentId, sessionKey) {
|
|
241
|
+
try {
|
|
242
|
+
const sessionsJsonPath = path.join(agentsStateDir, agentId, 'sessions', 'sessions.json');
|
|
243
|
+
const raw = JSON.parse(fs.readFileSync(sessionsJsonPath, 'utf8'));
|
|
244
|
+
const entry = raw[sessionKey];
|
|
245
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
246
|
+
return null;
|
|
247
|
+
return toNonEmptyString(entry.sessionId);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function listDeletedSessionPaths(agentsStateDir, agentId, sessionId) {
|
|
254
|
+
const sessionsDir = path.join(agentsStateDir, agentId, 'sessions');
|
|
255
|
+
if (!fs.existsSync(sessionsDir))
|
|
256
|
+
return [];
|
|
257
|
+
const prefix = `${sessionId}.jsonl.deleted.`;
|
|
258
|
+
try {
|
|
259
|
+
return fs.readdirSync(sessionsDir)
|
|
260
|
+
.filter((name) => name.startsWith(prefix))
|
|
261
|
+
.sort((a, b) => b.localeCompare(a))
|
|
262
|
+
.map((name) => path.join(sessionsDir, name));
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function collectTaskSessionRefs(task) {
|
|
269
|
+
const refs = [];
|
|
270
|
+
if (task.orchestratorSessionKey && task.orchestratorId) {
|
|
271
|
+
refs.push({
|
|
272
|
+
scope: 'orchestrator',
|
|
273
|
+
agentId: task.orchestratorId,
|
|
274
|
+
sessionKey: task.orchestratorSessionKey,
|
|
275
|
+
sessionId: task.orchestratorSessionId ?? null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
for (const [agentId, raw] of Object.entries(task.workerSessions ?? {})) {
|
|
279
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
280
|
+
continue;
|
|
281
|
+
const data = raw;
|
|
282
|
+
const sessionKey = toNonEmptyString(data.sessionKey);
|
|
283
|
+
if (!sessionKey)
|
|
284
|
+
continue;
|
|
285
|
+
refs.push({
|
|
286
|
+
scope: 'worker',
|
|
287
|
+
agentId,
|
|
288
|
+
sessionKey,
|
|
289
|
+
sessionId: toNonEmptyString(data.sessionId),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return refs;
|
|
293
|
+
}
|
|
294
|
+
function diagnoseTaskSessions(task, agentsStateDir) {
|
|
295
|
+
if (!agentsStateDir)
|
|
296
|
+
return [];
|
|
297
|
+
return collectTaskSessionRefs(task).map((ref) => {
|
|
298
|
+
const parsed = parseAgentSubagentSessionKey(ref.sessionKey);
|
|
299
|
+
const agentId = parsed?.agentId ?? ref.agentId;
|
|
300
|
+
let resolvedSessionId = ref.sessionId;
|
|
301
|
+
let mappingSource = ref.sessionId ? 'explicit' : 'none';
|
|
302
|
+
if (!resolvedSessionId && parsed) {
|
|
303
|
+
resolvedSessionId = parsed.sessionId;
|
|
304
|
+
mappingSource = 'parsed';
|
|
305
|
+
}
|
|
306
|
+
if (!resolvedSessionId) {
|
|
307
|
+
const mappedSessionId = resolveSessionIdFromSessionsJson(agentsStateDir, agentId, ref.sessionKey);
|
|
308
|
+
if (mappedSessionId) {
|
|
309
|
+
resolvedSessionId = mappedSessionId;
|
|
310
|
+
mappingSource = 'sessions_json';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!resolvedSessionId) {
|
|
314
|
+
return {
|
|
315
|
+
scope: ref.scope,
|
|
316
|
+
agentId,
|
|
317
|
+
sessionKey: ref.sessionKey,
|
|
318
|
+
sessionId: null,
|
|
319
|
+
mappingSource,
|
|
320
|
+
state: 'missing',
|
|
321
|
+
reason: 'missing_session_mapping',
|
|
322
|
+
activePath: null,
|
|
323
|
+
deletedPath: null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const activePath = path.join(agentsStateDir, agentId, 'sessions', `${resolvedSessionId}.jsonl`);
|
|
327
|
+
if (fs.existsSync(activePath)) {
|
|
328
|
+
return {
|
|
329
|
+
scope: ref.scope,
|
|
330
|
+
agentId,
|
|
331
|
+
sessionKey: ref.sessionKey,
|
|
332
|
+
sessionId: resolvedSessionId,
|
|
333
|
+
mappingSource,
|
|
334
|
+
state: 'active',
|
|
335
|
+
reason: null,
|
|
336
|
+
activePath,
|
|
337
|
+
deletedPath: null,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const deletedPaths = listDeletedSessionPaths(agentsStateDir, agentId, resolvedSessionId);
|
|
341
|
+
if (deletedPaths.length > 0) {
|
|
342
|
+
return {
|
|
343
|
+
scope: ref.scope,
|
|
344
|
+
agentId,
|
|
345
|
+
sessionKey: ref.sessionKey,
|
|
346
|
+
sessionId: resolvedSessionId,
|
|
347
|
+
mappingSource,
|
|
348
|
+
state: 'deleted',
|
|
349
|
+
reason: 'session_dead',
|
|
350
|
+
activePath: null,
|
|
351
|
+
deletedPath: deletedPaths[0],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
scope: ref.scope,
|
|
356
|
+
agentId,
|
|
357
|
+
sessionKey: ref.sessionKey,
|
|
358
|
+
sessionId: resolvedSessionId,
|
|
359
|
+
mappingSource,
|
|
360
|
+
state: 'missing',
|
|
361
|
+
reason: null,
|
|
362
|
+
activePath: null,
|
|
363
|
+
deletedPath: null,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function joinUniqueAgentIds(diagnoses, reason) {
|
|
368
|
+
const agentIds = Array.from(new Set(diagnoses.filter((item) => item.reason === reason).map((item) => item.agentId)));
|
|
369
|
+
return agentIds.join(', ') || 'unknown-agent';
|
|
370
|
+
}
|
|
371
|
+
function buildStaleGuidance(task, reasonCode, hours, diagnoses) {
|
|
372
|
+
if (reasonCode === 'session_dead') {
|
|
373
|
+
const agents = joinUniqueAgentIds(diagnoses, 'session_dead');
|
|
374
|
+
return {
|
|
375
|
+
reason: 'session_dead',
|
|
376
|
+
nextAction: `respawn the deleted OpenClaw session for ${agents} and re-register it before resuming ${task.taskId}`,
|
|
377
|
+
resumeHint: `start a fresh session for ${agents}, then update zigrix with the new session key/sessionId before continuing the blocked task`,
|
|
378
|
+
reportLine: `${task.taskId}: BLOCKED session_dead (${agents})`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (reasonCode === 'missing_session_mapping') {
|
|
382
|
+
const agents = joinUniqueAgentIds(diagnoses, 'missing_session_mapping');
|
|
383
|
+
return {
|
|
384
|
+
reason: 'missing_session_mapping',
|
|
385
|
+
nextAction: `repair or re-register the missing session mapping for ${agents} before resuming ${task.taskId}`,
|
|
386
|
+
resumeHint: `re-run worker/orchestrator registration with --session-key and --session-id so zigrix can resolve the backing OpenClaw session`,
|
|
387
|
+
reportLine: `${task.taskId}: BLOCKED missing_session_mapping (${agents})`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
reason: 'stale_timeout',
|
|
392
|
+
nextAction: `inspect the latest progress for ${task.taskId} and either report, refresh progress, or respawn the worker after ${hours}h of inactivity`,
|
|
393
|
+
resumeHint: 'check task status/events/evidence, then continue with a fresh worker registration if the original session is no longer active',
|
|
394
|
+
reportLine: `${task.taskId}: BLOCKED stale_timeout (> ${hours}h)`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function buildStaleTaskSummary(task, hours, agentsStateDir, fallbackReason = 'stale_timeout') {
|
|
194
398
|
const cutoff = Date.now() - hours * 3600 * 1000;
|
|
195
|
-
|
|
399
|
+
const timedOut = Date.parse(task.updatedAt) < cutoff;
|
|
400
|
+
const diagnoses = diagnoseTaskSessions(task, agentsStateDir);
|
|
401
|
+
const hasDeletedSession = diagnoses.some((item) => item.reason === 'session_dead');
|
|
402
|
+
const hasMissingMapping = diagnoses.some((item) => item.reason === 'missing_session_mapping');
|
|
403
|
+
if (!hasDeletedSession && !timedOut)
|
|
404
|
+
return null;
|
|
405
|
+
const reasons = new Set();
|
|
406
|
+
if (timedOut)
|
|
407
|
+
reasons.add('stale_timeout');
|
|
408
|
+
if (hasDeletedSession)
|
|
409
|
+
reasons.add('session_dead');
|
|
410
|
+
if (hasMissingMapping)
|
|
411
|
+
reasons.add('missing_session_mapping');
|
|
412
|
+
const reasonCode = hasDeletedSession
|
|
413
|
+
? 'session_dead'
|
|
414
|
+
: hasMissingMapping && timedOut
|
|
415
|
+
? 'missing_session_mapping'
|
|
416
|
+
: 'stale_timeout';
|
|
417
|
+
const guidance = buildStaleGuidance(task, reasonCode, hours, diagnoses);
|
|
418
|
+
const reason = reasonCode === 'stale_timeout' ? fallbackReason : guidance.reason;
|
|
419
|
+
return {
|
|
420
|
+
taskId: task.taskId,
|
|
421
|
+
title: task.title,
|
|
422
|
+
updatedAt: task.updatedAt,
|
|
423
|
+
hoursThreshold: hours,
|
|
424
|
+
timedOut,
|
|
425
|
+
reason,
|
|
426
|
+
reasonCode,
|
|
427
|
+
reasons: Array.from(reasons),
|
|
428
|
+
nextAction: guidance.nextAction,
|
|
429
|
+
resumeHint: guidance.resumeHint,
|
|
430
|
+
reportLine: guidance.reportLine,
|
|
431
|
+
sessions: diagnoses,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
export function findStaleTasks(paths, hours = 24, options = {}) {
|
|
435
|
+
return listTasks(paths)
|
|
436
|
+
.filter((task) => task.status === 'IN_PROGRESS')
|
|
437
|
+
.map((task) => buildStaleTaskSummary(task, hours, options.agentsStateDir, options.fallbackReason ?? 'stale_timeout'))
|
|
438
|
+
.filter((task) => task !== null);
|
|
196
439
|
}
|
|
197
|
-
export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout') {
|
|
198
|
-
const staleTasks = findStaleTasks(paths, hours);
|
|
199
|
-
const changed = staleTasks.map((
|
|
440
|
+
export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout', options = {}) {
|
|
441
|
+
const staleTasks = findStaleTasks(paths, hours, { ...options, fallbackReason: reason });
|
|
442
|
+
const changed = staleTasks.map((summary) => {
|
|
443
|
+
const task = loadTask(paths, summary.taskId);
|
|
444
|
+
if (!task)
|
|
445
|
+
return null;
|
|
200
446
|
task.status = 'BLOCKED';
|
|
447
|
+
task.nextAction = summary.nextAction;
|
|
448
|
+
task.resumeHint = summary.resumeHint;
|
|
449
|
+
task.staleReason = summary.reasonCode;
|
|
450
|
+
task.staleReasons = summary.reasons;
|
|
201
451
|
saveTask(paths, task);
|
|
202
452
|
const event = appendEvent(paths.eventsFile, {
|
|
203
453
|
event: 'task_blocked',
|
|
@@ -205,12 +455,33 @@ export function applyStalePolicy(paths, hours = 24, reason = 'stale_timeout') {
|
|
|
205
455
|
phase: 'recovery',
|
|
206
456
|
actor: 'zigrix',
|
|
207
457
|
status: 'BLOCKED',
|
|
208
|
-
payload: {
|
|
458
|
+
payload: {
|
|
459
|
+
reason: summary.reason,
|
|
460
|
+
reasonCode: summary.reasonCode,
|
|
461
|
+
reasons: summary.reasons,
|
|
462
|
+
previousStatus: 'IN_PROGRESS',
|
|
463
|
+
hoursThreshold: hours,
|
|
464
|
+
timedOut: summary.timedOut,
|
|
465
|
+
nextAction: summary.nextAction,
|
|
466
|
+
resumeHint: summary.resumeHint,
|
|
467
|
+
reportLine: summary.reportLine,
|
|
468
|
+
sessions: summary.sessions,
|
|
469
|
+
},
|
|
209
470
|
});
|
|
210
|
-
return {
|
|
211
|
-
|
|
471
|
+
return {
|
|
472
|
+
taskId: task.taskId,
|
|
473
|
+
reason: summary.reason,
|
|
474
|
+
reasonCode: summary.reasonCode,
|
|
475
|
+
reasons: summary.reasons,
|
|
476
|
+
nextAction: summary.nextAction,
|
|
477
|
+
resumeHint: summary.resumeHint,
|
|
478
|
+
reportLine: summary.reportLine,
|
|
479
|
+
sessions: summary.sessions,
|
|
480
|
+
event,
|
|
481
|
+
};
|
|
482
|
+
}).filter((item) => item !== null);
|
|
212
483
|
rebuildIndex(paths);
|
|
213
|
-
return { ok: true, hours, reason, count: changed.length, changed };
|
|
484
|
+
return { ok: true, hours, requestedReason: reason, count: changed.length, changed };
|
|
214
485
|
}
|
|
215
486
|
// ─── Index ──────────────────────────────────────────────────────────────────
|
|
216
487
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zigrix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-project parallel task orchestration CLI for agent-assisted development",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"@inquirer/prompts": "^8.3.2",
|
|
71
71
|
"bcryptjs": "^2.4.3",
|
|
72
72
|
"commander": "^14.0.0",
|
|
73
|
-
"next": "^15.
|
|
73
|
+
"next": "^15.5.15",
|
|
74
74
|
"react": "^19.0.0",
|
|
75
75
|
"react-dom": "^19.0.0",
|
|
76
76
|
"react-markdown": "^10.1.0",
|
|
77
|
-
"yaml": "^2.8.
|
|
77
|
+
"yaml": "^2.8.3",
|
|
78
78
|
"zod": "^4.1.5"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# orchestrator-agent Rules (Orchestrator)
|
|
2
2
|
|
|
3
3
|
> orchestrator-agent는 오케스트레이터로서 worker-common이 아닌 자체 규칙을 따른다.
|
|
4
|
+
> 이 문서 안의 `frontend-agent`/`backend-agent`/`qa-agent`/`orchestrator-agent` 표기는 **role label 예시**다. 실제 runtime agentId는 dispatch overlay의 `roleAgentMap`, `orchestratorId`, `qaAgentId`를 따른다.
|
|
4
5
|
|
|
5
6
|
## 1) Mission
|
|
6
7
|
- 사용자 개발 요청을 `simple|normal|risky|large`로 분류하고,
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
11. **오케스트레이션 파이프라인 필수 경유:**
|
|
24
25
|
- 작업은 `zigrix task dispatch`로 등록한 상태에서만 수신
|
|
25
26
|
- 오케스트레이션 미등록(taskId/spec 미존재) 작업은 수행 거부
|
|
26
|
-
12.
|
|
27
|
+
12. **메인 전용 스킬 사용 금지:** `zigrix-main-agent-guide`는 main agent 전용이며, orchestrator runtime 세션은 dispatch prompt + Zigrix role rule만 canonical instruction으로 사용한다.
|
|
28
|
+
13. **CLI 체인 워크플로우 (필수):**
|
|
27
29
|
- orchestrator-agent의 task prompt(boot prompt)는 **`zigrix task start` 실행 지시**만 포함한다.
|
|
28
30
|
- **태스크 메타 및 dispatch prompt가 태스크 브리핑이자 작업 지시서**이다.
|
|
29
31
|
- 모든 상태 추적은 CLI 체인을 통해 자동 기록된다.
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
1. **착수:** `zigrix task start <taskId> --json`
|
|
34
36
|
2. **태스크 경로 확인:** `zigrix task status <taskId> --json` → `specPath`, `metaPath`, `projectDir` 확보
|
|
35
37
|
3. **워커 prompt 생성:** `zigrix worker prepare --task-id <taskId> --agent-id <workerId> --description "..." --json` → `promptPath`, `specPath`, `metaPath`, `projectDir`를 확인하고 sessions_spawn에 prompt를 전달
|
|
36
|
-
4. **워커 등록:** `zigrix worker register --task-id <taskId> --agent-id <workerId> --session-key <key> --run-id <rid>` → 다음 행동 출력
|
|
38
|
+
4. **워커 등록:** `zigrix worker register --task-id <taskId> --agent-id <workerId> --session-key <key> --label <spawnLabel> --project-dir <projectDir> --run-id <rid>` → 다음 행동 출력
|
|
37
39
|
5. **워커 완료:** `zigrix worker complete --task-id <taskId> --agent-id <workerId> --session-key <key> --run-id <rid>` → 완료 여부 + 다음 행동 출력
|
|
38
40
|
6. **최종 보고:** `zigrix task finalize <taskId> --auto-report`
|
|
39
41
|
13. task는 크게 유지하고 내부 실행은 `workPackages[]` + `executionUnits[]`로 세분화한다.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Worker Common Rules (front/back/sys/sec/qa)
|
|
2
2
|
|
|
3
|
+
> 이 문서 안의 역할명은 role label 기준이다. 실제 runtime agentId / projectDir / task context는 worker overlay prompt를 우선 따른다.
|
|
4
|
+
|
|
3
5
|
## 1) Mission
|
|
4
6
|
- orchestrator-agent가 분배한 task를 역할 범위 내에서 수행하고,
|
|
5
7
|
- 결과를 검증 가능한 증적과 함께 반환한다.
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
6. **모든 문제는 근본적인 해결을 원칙으로 한다. 임시방편(workaround) 금지.**
|
|
14
16
|
7. **오케스트레이션 필수 (owner 고정, 2026-03-04):** 오케스트레이션에 등록되지 않은 작업은 수행 거부. 확인은 `zigrix task status <taskId> --json`의 성공 여부와 반환된 `specPath`/`metaPath`를 기준으로 한다. taskId가 있더라도 조회가 실패하면 orchestrator-agent에 확인 요청.
|
|
15
17
|
8. **CLI 체인 정합:** 워커 lifecycle 기록(`worker_dispatched`/`worker_done`/`worker_skipped`)은 orchestrator-agent가 `zigrix worker prepare → zigrix worker register → zigrix worker complete` 체인으로 처리한다.
|
|
18
|
+
9. **메인 전용 스킬 사용 금지:** `zigrix-main-agent-guide`는 main agent 전용이며, worker runtime 세션은 worker overlay prompt + Zigrix role rule만 canonical instruction으로 사용한다.
|
|
16
19
|
9. **Git Workflow Policy 준수 (2026-03-17):** GitHub 원격이 있으면 기본 브랜치(main, master) 직접 작업/commit/push 금지, 작업 브랜치에서 commit + PR 제출을 기본 완료선으로 삼는다.
|
|
17
20
|
10. **완료 상태 불변성 (2026-03-17):** `REPORTED` task에 대한 후행 completion/event는 상태 전이를 만들지 않는다. 워커는 중복 완료 알림이 와도 추가 상태 변경 시도를 하지 않고 NO-OP로 처리한다.
|
|
18
21
|
11. **Git 조작 금지 (2026-03-23):** non-orchestrator 역할 워커(frontend/backend/system/security/qa)는 `git commit`, `git push`, `git branch`, `git checkout -b`, PR 생성 등 **Git 상태를 변경하는 모든 조작을 수행하지 않는다.** 워커의 역할은 파일 수정/생성/삭제까지이며, 작업 완료 시 **변경된 파일 목록**을 orchestrator 역할에 반환한다. commit/push/PR은 orchestrator가 QA 통과 확인 후 전담한다.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oz
|
|
3
|
+
version: 0.2.0
|
|
4
|
+
description: Official OpenClaw Zigrix entrypoint. Force Zigrix delegation when a message starts with `/oz`, and semantically route plain-language requests to hand work off, assign it, or have Zigrix take it instead of doing the work directly.
|
|
5
|
+
metadata:
|
|
6
|
+
openclaw:
|
|
7
|
+
requires:
|
|
8
|
+
bins: ["zigrix"]
|
|
9
|
+
cliHelp: "zigrix task dispatch --help"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /oz — OpenClaw Zigrix Entrypoint
|
|
13
|
+
|
|
14
|
+
Use this skill when either of the following is true:
|
|
15
|
+
|
|
16
|
+
1. the user message starts with `/oz `
|
|
17
|
+
2. the user is clearly asking, in natural language, to have work **delegated / handed off / assigned / orchestrated through Zigrix** instead of having the current main agent do it directly
|
|
18
|
+
|
|
19
|
+
This skill is the public OpenClaw-facing entrypoint for Zigrix after `zigrix onboard` installs bundled skills.
|
|
20
|
+
|
|
21
|
+
## 1) Route selection
|
|
22
|
+
|
|
23
|
+
### `/oz` prefix
|
|
24
|
+
If the message starts with `/oz `, treat it as:
|
|
25
|
+
- `route = delegate`
|
|
26
|
+
- `delegateMode = force`
|
|
27
|
+
|
|
28
|
+
Remove the `/oz` prefix and use the remaining text as the delegation payload.
|
|
29
|
+
|
|
30
|
+
### Natural-language delegation
|
|
31
|
+
If there is no `/oz` prefix, judge the user's intent **semantically**.
|
|
32
|
+
|
|
33
|
+
- Use the full meaning of the request and nearby context.
|
|
34
|
+
- Do **not** rely on keyword tables or regex lists as the decision mechanism.
|
|
35
|
+
- When helpful, read:
|
|
36
|
+
- `references/routing-rubric.md`
|
|
37
|
+
- `references/examples.md`
|
|
38
|
+
|
|
39
|
+
Classify the turn into one of:
|
|
40
|
+
- `delegate`
|
|
41
|
+
- `direct`
|
|
42
|
+
- `answer`
|
|
43
|
+
|
|
44
|
+
### Semantic routing policy
|
|
45
|
+
- Choose **`delegate`** when the user wants work to be handed off, assigned, orchestrated, or otherwise run through Zigrix rather than performed directly by the current main agent.
|
|
46
|
+
- Choose **`direct`** only when the user explicitly wants the current agent to do it directly.
|
|
47
|
+
- Choose **`answer`** when the user is asking for explanation, status, architecture, policy, or other non-execution discussion.
|
|
48
|
+
- If the request is ambiguous but points toward tracked execution / implementation / multi-step change, bias toward `delegate` unless the user explicitly asked for direct execution.
|
|
49
|
+
|
|
50
|
+
## 2) Delegate flow
|
|
51
|
+
|
|
52
|
+
If `route = delegate`, use the canonical Zigrix handoff chain:
|
|
53
|
+
|
|
54
|
+
1. Turn the user request into:
|
|
55
|
+
- `title`
|
|
56
|
+
- `description`
|
|
57
|
+
- `scale`
|
|
58
|
+
- optional `projectDir`
|
|
59
|
+
2. Run:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
zigrix task dispatch \
|
|
63
|
+
--title "..." \
|
|
64
|
+
--description "..." \
|
|
65
|
+
--scale simple|normal|risky|large \
|
|
66
|
+
--project-dir /path/to/project \
|
|
67
|
+
--json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
3. Read the JSON result and extract:
|
|
71
|
+
- `taskId`
|
|
72
|
+
- `orchestratorId`
|
|
73
|
+
- `orchestratorPrompt`
|
|
74
|
+
- `projectDir`
|
|
75
|
+
4. Spawn the orchestrator with the returned prompt.
|
|
76
|
+
|
|
77
|
+
Preferred pattern:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
sessions_spawn(
|
|
81
|
+
agentId: <orchestratorId>,
|
|
82
|
+
mode: "session",
|
|
83
|
+
thread: true when the current surface supports stable Zigrix thread orchestration, otherwise false,
|
|
84
|
+
label: "[<orchestratorId>] <taskId>",
|
|
85
|
+
task: <orchestratorPrompt>,
|
|
86
|
+
cwd: <projectDir when present>
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
5. Reply briefly with the handoff result (`taskId`, orchestrator/session/thread info).
|
|
91
|
+
|
|
92
|
+
## 3) Delegate-only guard
|
|
93
|
+
|
|
94
|
+
Once this skill decides `route = delegate`:
|
|
95
|
+
- do **not** directly implement the task
|
|
96
|
+
- do **not** directly edit/write project files as a substitute for Zigrix handoff
|
|
97
|
+
- do **not** fallback to direct execution if dispatch or spawn fails
|
|
98
|
+
|
|
99
|
+
If dispatch or spawn fails:
|
|
100
|
+
- report the failure clearly
|
|
101
|
+
- include the failing step
|
|
102
|
+
- stop there
|
|
103
|
+
|
|
104
|
+
## 4) Empty or underspecified payloads
|
|
105
|
+
|
|
106
|
+
If `/oz` is used but the remaining payload is empty or too underspecified to create a task:
|
|
107
|
+
- ask the user to restate the task in one clear sentence
|
|
108
|
+
- do not silently guess
|
|
109
|
+
- do not direct-execute instead
|
|
110
|
+
|
|
111
|
+
## 5) Direct / answer cases
|
|
112
|
+
|
|
113
|
+
If semantic routing chooses:
|
|
114
|
+
- `direct` → current agent may proceed with direct execution
|
|
115
|
+
- `answer` → answer normally without creating a Zigrix task
|
|
116
|
+
|
|
117
|
+
This skill exists to decide **whether the request should enter Zigrix orchestration** and, when yes, to force the canonical delegation path.
|