zigrix 0.2.0 → 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/dashboard/.next/BUILD_ID +1 -1
- package/dist/dashboard/.next/app-build-manifest.json +24 -24
- package/dist/dashboard/.next/app-path-routes-manifest.json +2 -2
- package/dist/dashboard/.next/build-manifest.json +5 -5
- 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/login.html +1 -1
- package/dist/dashboard/.next/server/app/login.rsc +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/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/index.js +25 -2
- package/dist/onboard.d.ts +1 -1
- package/dist/onboard.js +2 -2
- package/dist/orchestration/dispatch.js +16 -71
- 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 +6 -0
- package/dist/state/tasks.js +39 -0
- 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/{EZjkAnODdTglaMXuBw76E → dOjvoQUj-mqwJ8kKG4peU}/_buildManifest.js +0 -0
- /package/dist/dashboard/.next/static/{EZjkAnODdTglaMXuBw76E → dOjvoQUj-mqwJ8kKG4peU}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { inferStandardAgentRole, normalizeAgentRole, } from '../agents/roles.js';
|
|
5
|
+
const ROLE_RULE_FILE = {
|
|
6
|
+
orchestrator: 'orchestrator-agent.md',
|
|
7
|
+
qa: 'qa-agent.md',
|
|
8
|
+
security: 'security-agent.md',
|
|
9
|
+
frontend: 'frontend-agent.md',
|
|
10
|
+
backend: 'backend-agent.md',
|
|
11
|
+
system: 'system-agent.md',
|
|
12
|
+
};
|
|
13
|
+
const WORKER_COMMON_RULE_FILE = 'worker-common.md';
|
|
14
|
+
function bundledRulesDir() {
|
|
15
|
+
return path.resolve(fileURLToPath(new URL('../../rules/defaults', import.meta.url)));
|
|
16
|
+
}
|
|
17
|
+
function firstReadableRulePath(paths, fileName) {
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.join(paths.rulesDir, fileName),
|
|
20
|
+
path.join(bundledRulesDir(), fileName),
|
|
21
|
+
];
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function loadRuleText(paths, fileName) {
|
|
30
|
+
const resolved = firstReadableRulePath(paths, fileName);
|
|
31
|
+
if (!resolved) {
|
|
32
|
+
throw new Error(`required Zigrix rule file not found: ${fileName}`);
|
|
33
|
+
}
|
|
34
|
+
return fs.readFileSync(resolved, 'utf8').trim();
|
|
35
|
+
}
|
|
36
|
+
function compactBlock(text) {
|
|
37
|
+
return text.replace(/\r\n/g, '\n').trim();
|
|
38
|
+
}
|
|
39
|
+
function joinPromptBlocks(...blocks) {
|
|
40
|
+
return blocks
|
|
41
|
+
.map((block) => (typeof block === 'string' ? compactBlock(block) : ''))
|
|
42
|
+
.filter((block) => block.length > 0)
|
|
43
|
+
.join('\n\n---\n\n');
|
|
44
|
+
}
|
|
45
|
+
function renderRoleMap(task) {
|
|
46
|
+
const entries = Object.entries(task.roleAgentMap ?? {}).filter(([, ids]) => Array.isArray(ids) && ids.length > 0);
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
return '- (none)';
|
|
49
|
+
return entries.map(([role, ids]) => `- ${role}: ${ids.join(', ')}`).join('\n');
|
|
50
|
+
}
|
|
51
|
+
function resolveTaskRole(task, agentId) {
|
|
52
|
+
if (typeof task.orchestratorId === 'string' && task.orchestratorId === agentId)
|
|
53
|
+
return 'orchestrator';
|
|
54
|
+
if (typeof task.qaAgentId === 'string' && task.qaAgentId === agentId)
|
|
55
|
+
return 'qa';
|
|
56
|
+
const roleMap = task.roleAgentMap;
|
|
57
|
+
if (roleMap && typeof roleMap === 'object') {
|
|
58
|
+
for (const [role, agentIds] of Object.entries(roleMap)) {
|
|
59
|
+
if (!Array.isArray(agentIds))
|
|
60
|
+
continue;
|
|
61
|
+
if (agentIds.includes(agentId)) {
|
|
62
|
+
const normalized = normalizeAgentRole(role);
|
|
63
|
+
if (normalized)
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
export function resolveAgentRole(config, task, agentId) {
|
|
71
|
+
const fromTask = resolveTaskRole(task, agentId);
|
|
72
|
+
if (fromTask)
|
|
73
|
+
return fromTask;
|
|
74
|
+
const fromRegistry = config.agents.registry[agentId]?.role;
|
|
75
|
+
if (typeof fromRegistry === 'string') {
|
|
76
|
+
const normalized = normalizeAgentRole(fromRegistry);
|
|
77
|
+
if (normalized)
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
return inferStandardAgentRole({ agentId, theme: null });
|
|
81
|
+
}
|
|
82
|
+
export function buildSpawnLabel(taskId, agentId) {
|
|
83
|
+
return `[${agentId}] ${taskId}`;
|
|
84
|
+
}
|
|
85
|
+
export function composeOrchestratorPrompt(params) {
|
|
86
|
+
const roleRule = loadRuleText(params.paths, ROLE_RULE_FILE.orchestrator);
|
|
87
|
+
const overlay = [
|
|
88
|
+
`## Runtime Task Overlay: ${params.task.taskId}`,
|
|
89
|
+
'',
|
|
90
|
+
'| Field | Value |',
|
|
91
|
+
'|---|---|',
|
|
92
|
+
`| taskId | ${params.task.taskId} |`,
|
|
93
|
+
`| title | ${params.task.title} |`,
|
|
94
|
+
`| scale | ${params.task.scale} |`,
|
|
95
|
+
`| orchestratorId | ${params.orchestratorId} |`,
|
|
96
|
+
`| qaAgentId | ${params.qaAgentId} |`,
|
|
97
|
+
`| specPath | ${params.specPath} |`,
|
|
98
|
+
`| metaPath | ${params.metaPath} |`,
|
|
99
|
+
`| promptPath | ${params.promptPath} |`,
|
|
100
|
+
...(params.projectDir ? [`| projectDir | ${params.projectDir} |`] : []),
|
|
101
|
+
'',
|
|
102
|
+
'### Authority Boundary',
|
|
103
|
+
'- 이 prompt는 Zigrix orchestrator role rule + task overlay를 합성한 canonical instruction이다.',
|
|
104
|
+
'- 메인 전용 skill (`zigrix-main-agent-guide`)이나 일반 OpenClaw skill discovery를 근거 규칙으로 삼지 않는다.',
|
|
105
|
+
'- 실제 runtime agentId는 아래 role mapping과 task metadata를 따른다.',
|
|
106
|
+
'',
|
|
107
|
+
'### Original Request',
|
|
108
|
+
params.task.description,
|
|
109
|
+
params.constraints ? `\n### Constraints\n${params.constraints}` : '',
|
|
110
|
+
'',
|
|
111
|
+
'### Runtime Role Mapping',
|
|
112
|
+
renderRoleMap(params.task),
|
|
113
|
+
'',
|
|
114
|
+
'### Required CLI Chain',
|
|
115
|
+
'1. `zigrix task start <taskId> --json`',
|
|
116
|
+
'2. `zigrix task status <taskId> --json`',
|
|
117
|
+
'3. 필요 워커마다 `zigrix worker prepare → sessions_spawn → zigrix worker register → zigrix worker complete`',
|
|
118
|
+
'4. evidence 수집/머지 확인',
|
|
119
|
+
'5. `zigrix task finalize <taskId> --json`',
|
|
120
|
+
'',
|
|
121
|
+
'### Worker Dispatch Contract',
|
|
122
|
+
'- worker spawn 전에 반드시 `zigrix worker prepare --task-id <taskId> --agent-id <workerId> --description "..." --json` 를 호출해 worker prompt / projectDir / spawnLabel을 확보한다.',
|
|
123
|
+
'- worker spawn 시 `agentId`, `label`, `cwd`, `task` 를 모두 명시한다.',
|
|
124
|
+
'- spawn 후에는 prepare 응답의 `spawnLabel` / `projectDir` 와 실제 `sessionKey` / `sessionId` 를 함께 `zigrix worker register` 에 전달한다.',
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join('\n');
|
|
128
|
+
return joinPromptBlocks(roleRule, overlay);
|
|
129
|
+
}
|
|
130
|
+
export function composeWorkerPrompt(params) {
|
|
131
|
+
const role = resolveAgentRole(params.config, params.task, params.agentId);
|
|
132
|
+
const roleRule = loadRuleText(params.paths, ROLE_RULE_FILE[role]);
|
|
133
|
+
const baseRule = role === 'orchestrator' ? '' : loadRuleText(params.paths, WORKER_COMMON_RULE_FILE);
|
|
134
|
+
const overlay = [
|
|
135
|
+
`## Runtime Worker Overlay: ${params.task.taskId}`,
|
|
136
|
+
'',
|
|
137
|
+
'| Field | Value |',
|
|
138
|
+
'|---|---|',
|
|
139
|
+
`| taskId | ${params.task.taskId} |`,
|
|
140
|
+
`| title | ${params.task.title} |`,
|
|
141
|
+
`| scale | ${params.task.scale} |`,
|
|
142
|
+
`| agentId | ${params.agentId} |`,
|
|
143
|
+
`| role | ${role} |`,
|
|
144
|
+
`| specPath | ${params.specPath} |`,
|
|
145
|
+
`| metaPath | ${params.metaPath} |`,
|
|
146
|
+
`| promptPath | ${params.promptPath} |`,
|
|
147
|
+
...(params.projectDir ? [`| projectDir | ${params.projectDir} |`] : []),
|
|
148
|
+
'',
|
|
149
|
+
'### Authority Boundary',
|
|
150
|
+
'- 이 prompt는 Zigrix role rule + task overlay를 합성한 canonical worker instruction이다.',
|
|
151
|
+
'- 메인 전용 skill (`zigrix-main-agent-guide`)이나 일반 OpenClaw skill discovery를 근거 규칙으로 삼지 않는다.',
|
|
152
|
+
'- role rule 안의 예시 agent 이름보다 현재 overlay의 `agentId` / `role` / `projectDir` 를 우선 적용한다.',
|
|
153
|
+
'',
|
|
154
|
+
'### Assignment',
|
|
155
|
+
params.description,
|
|
156
|
+
params.constraints ? `\n### Constraints\n${params.constraints}` : '',
|
|
157
|
+
params.dod ? `\n### Definition of Done\n${params.dod}` : '',
|
|
158
|
+
params.unitId || params.workPackage
|
|
159
|
+
? `\n### Execution Context\n- unitId: ${params.unitId ?? 'N/A'}\n- workPackage: ${params.workPackage ?? 'N/A'}`
|
|
160
|
+
: '',
|
|
161
|
+
'',
|
|
162
|
+
'### Completion Chain',
|
|
163
|
+
`- evidence collect: \`zigrix evidence collect --task-id ${params.task.taskId} --agent-id ${params.agentId} --summary "<result summary>"\``,
|
|
164
|
+
'- 결과 보고에는 taskId / sessionKey / runId / evidence / risks 를 포함한다.',
|
|
165
|
+
]
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join('\n');
|
|
168
|
+
return {
|
|
169
|
+
role,
|
|
170
|
+
prompt: joinPromptBlocks(baseRule, roleRule, overlay),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ZigrixConfig } from '../config/schema.js';
|
|
1
2
|
import { type ZigrixPaths } from '../state/paths.js';
|
|
2
3
|
import { type ZigrixTask } from '../state/tasks.js';
|
|
3
4
|
export declare const DEFAULT_REQUIRED_ROLES: readonly ["orchestrator", "qa"];
|
|
4
5
|
export declare function resolveRequiredAgents(task: Partial<ZigrixTask> & Record<string, unknown>): string[];
|
|
5
|
-
export declare function prepareWorker(paths: ZigrixPaths, params: {
|
|
6
|
+
export declare function prepareWorker(paths: ZigrixPaths, config: ZigrixConfig, params: {
|
|
6
7
|
taskId: string;
|
|
7
8
|
agentId: string;
|
|
8
9
|
description: string;
|
|
@@ -21,6 +22,8 @@ export declare function registerWorker(paths: ZigrixPaths, params: {
|
|
|
21
22
|
unitId?: string;
|
|
22
23
|
workPackage?: string;
|
|
23
24
|
reason?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
projectDir?: string;
|
|
24
27
|
}): Record<string, unknown> | null;
|
|
25
28
|
export declare function completeWorker(paths: ZigrixPaths, params: {
|
|
26
29
|
taskId: string;
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { appendEvent } from '../state/events.js';
|
|
4
4
|
import { ensureBaseState } from '../state/paths.js';
|
|
5
5
|
import { loadTask, resolveTaskPaths, saveTask } from '../state/tasks.js';
|
|
6
|
+
import { buildSpawnLabel, composeWorkerPrompt } from './prompt-compose.js';
|
|
6
7
|
export const DEFAULT_REQUIRED_ROLES = ['orchestrator', 'qa'];
|
|
7
8
|
function firstNonEmpty(...values) {
|
|
8
9
|
for (const value of values) {
|
|
@@ -47,45 +48,51 @@ export function resolveRequiredAgents(task) {
|
|
|
47
48
|
}
|
|
48
49
|
return resolveDefaultRequiredAgents(task);
|
|
49
50
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
...(params.projectDir ? [`| projectDir | ${params.projectDir} |`] : []),
|
|
61
|
-
'',
|
|
62
|
-
'### Assignment',
|
|
63
|
-
params.description,
|
|
64
|
-
];
|
|
65
|
-
if (params.constraints)
|
|
66
|
-
sections.push('', '### Constraints', params.constraints);
|
|
67
|
-
if (params.dod)
|
|
68
|
-
sections.push('', '### Definition of Done', params.dod);
|
|
69
|
-
if (params.unitId || params.workPackage) {
|
|
70
|
-
sections.push('', '### Execution Context', `- unitId: ${params.unitId ?? 'N/A'}`, `- workPackage: ${params.workPackage ?? 'N/A'}`);
|
|
71
|
-
}
|
|
72
|
-
sections.push('', '### Completion', '작업 완료 후 다음 순서를 반드시 따르라:', '', '1. **증적(evidence) 수집** — 작업 결과 증적을 먼저 기록한다:', ' ```bash', ` zigrix evidence collect --task-id ${params.task.taskId} --agent-id ${params.agentId} --summary "<작업 결과 요약>"`, ' ```', '2. **결과 보고** — 증적 수집 완료 후 결과와 근거를 명확히 보고하라.', '', '⚠️ 증적 없이 완료하면 finalize에서 incomplete 판정된다.');
|
|
73
|
-
return sections.join('\n');
|
|
51
|
+
function normalizePathValue(value) {
|
|
52
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return path.resolve(value.trim());
|
|
55
|
+
}
|
|
56
|
+
function parseSessionKey(sessionKey) {
|
|
57
|
+
const matched = sessionKey.match(/^agent:([^:]+):subagent:([^:\s]+)$/);
|
|
58
|
+
if (!matched)
|
|
59
|
+
return null;
|
|
60
|
+
return { agentId: matched[1], sessionId: matched[2] };
|
|
74
61
|
}
|
|
75
|
-
export function prepareWorker(paths, params) {
|
|
62
|
+
export function prepareWorker(paths, config, params) {
|
|
76
63
|
ensureBaseState(paths);
|
|
77
64
|
const task = loadTask(paths, params.taskId);
|
|
78
65
|
if (!task)
|
|
79
66
|
return null;
|
|
80
|
-
const
|
|
81
|
-
const prompt = renderPrompt({ task, ...params, projectDir });
|
|
67
|
+
const taskPaths = resolveTaskPaths(paths, params.taskId);
|
|
82
68
|
const promptPath = path.join(paths.promptsDir, `${params.taskId}-${params.agentId}.md`);
|
|
83
|
-
|
|
69
|
+
const projectDir = normalizePathValue(params.projectDir ?? task.projectDir ?? null);
|
|
70
|
+
const spawnLabel = buildSpawnLabel(params.taskId, params.agentId);
|
|
71
|
+
const composed = composeWorkerPrompt({
|
|
72
|
+
paths,
|
|
73
|
+
config,
|
|
74
|
+
task,
|
|
75
|
+
agentId: params.agentId,
|
|
76
|
+
description: params.description,
|
|
77
|
+
constraints: params.constraints,
|
|
78
|
+
unitId: params.unitId,
|
|
79
|
+
workPackage: params.workPackage,
|
|
80
|
+
dod: params.dod,
|
|
81
|
+
projectDir,
|
|
82
|
+
promptPath,
|
|
83
|
+
specPath: taskPaths.specPath,
|
|
84
|
+
metaPath: taskPaths.metaPath,
|
|
85
|
+
});
|
|
86
|
+
fs.writeFileSync(promptPath, `${composed.prompt}\n`, 'utf8');
|
|
84
87
|
task.workerSessions[params.agentId] = {
|
|
88
|
+
...(task.workerSessions[params.agentId] ?? {}),
|
|
85
89
|
status: 'prepared',
|
|
90
|
+
role: composed.role,
|
|
86
91
|
unitId: params.unitId,
|
|
87
92
|
workPackage: params.workPackage,
|
|
88
93
|
promptPath,
|
|
94
|
+
expectedLabel: spawnLabel,
|
|
95
|
+
projectDir,
|
|
89
96
|
};
|
|
90
97
|
const required = task.requiredAgents.length > 0 ? task.requiredAgents : resolveRequiredAgents(task);
|
|
91
98
|
if (!required.includes(params.agentId))
|
|
@@ -101,36 +108,60 @@ export function prepareWorker(paths, params) {
|
|
|
101
108
|
status: 'IN_PROGRESS',
|
|
102
109
|
unitId: params.unitId,
|
|
103
110
|
workPackage: params.workPackage,
|
|
104
|
-
payload: {
|
|
111
|
+
payload: {
|
|
112
|
+
agentId: params.agentId,
|
|
113
|
+
role: composed.role,
|
|
114
|
+
description: params.description,
|
|
115
|
+
constraints: params.constraints ?? '',
|
|
116
|
+
dod: params.dod ?? '',
|
|
117
|
+
promptPath,
|
|
118
|
+
spawnLabel,
|
|
119
|
+
projectDir,
|
|
120
|
+
},
|
|
105
121
|
});
|
|
106
122
|
return {
|
|
107
123
|
ok: true,
|
|
108
124
|
taskId: params.taskId,
|
|
109
125
|
agentId: params.agentId,
|
|
126
|
+
role: composed.role,
|
|
110
127
|
promptPath,
|
|
111
|
-
prompt,
|
|
128
|
+
prompt: composed.prompt,
|
|
112
129
|
unitId: params.unitId,
|
|
113
130
|
workPackage: params.workPackage,
|
|
114
|
-
projectDir
|
|
115
|
-
|
|
131
|
+
projectDir,
|
|
132
|
+
spawnLabel,
|
|
133
|
+
...taskPaths,
|
|
116
134
|
};
|
|
117
135
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Extract sessionId from a sessionKey of the form `agent:<agentId>:subagent:<sessionId>`.
|
|
120
|
-
* Returns null if the sessionKey does not match the expected pattern.
|
|
121
|
-
*/
|
|
122
|
-
function parseSessionIdFromKey(sessionKey) {
|
|
123
|
-
const matched = sessionKey.match(/^agent:[^:]+:subagent:([^:\s]+)$/);
|
|
124
|
-
return matched?.[1] ?? null;
|
|
125
|
-
}
|
|
126
136
|
export function registerWorker(paths, params) {
|
|
127
137
|
const task = loadTask(paths, params.taskId);
|
|
128
138
|
if (!task)
|
|
129
139
|
return null;
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
const parsedKey = parseSessionKey(params.sessionKey);
|
|
141
|
+
if (parsedKey && parsedKey.agentId !== params.agentId) {
|
|
142
|
+
throw new Error(`worker register validation failed: sessionKey belongs to '${parsedKey.agentId}', expected '${params.agentId}'`);
|
|
143
|
+
}
|
|
144
|
+
const previous = task.workerSessions[params.agentId] ?? {};
|
|
145
|
+
const expectedLabel = firstNonEmpty(previous.expectedLabel) ?? buildSpawnLabel(params.taskId, params.agentId);
|
|
146
|
+
const providedLabel = firstNonEmpty(params.label);
|
|
147
|
+
if (!providedLabel) {
|
|
148
|
+
throw new Error('worker register validation failed: label is required (use spawnLabel from worker prepare).');
|
|
149
|
+
}
|
|
150
|
+
if (providedLabel !== expectedLabel) {
|
|
151
|
+
throw new Error(`worker register validation failed: label mismatch (expected '${expectedLabel}', got '${providedLabel}')`);
|
|
152
|
+
}
|
|
153
|
+
const expectedProjectDir = normalizePathValue(firstNonEmpty(previous.projectDir, task.projectDir));
|
|
154
|
+
const providedProjectDir = normalizePathValue(params.projectDir);
|
|
155
|
+
if (expectedProjectDir && !providedProjectDir) {
|
|
156
|
+
throw new Error('worker register validation failed: projectDir is required for this task.');
|
|
157
|
+
}
|
|
158
|
+
if (expectedProjectDir && providedProjectDir && expectedProjectDir !== providedProjectDir) {
|
|
159
|
+
throw new Error(`worker register validation failed: projectDir mismatch (expected '${expectedProjectDir}', got '${providedProjectDir}')`);
|
|
160
|
+
}
|
|
161
|
+
const resolvedSessionId = params.sessionId || parsedKey?.sessionId || null;
|
|
162
|
+
const resolvedProjectDir = providedProjectDir ?? expectedProjectDir;
|
|
132
163
|
task.workerSessions[params.agentId] = {
|
|
133
|
-
...
|
|
164
|
+
...previous,
|
|
134
165
|
status: 'dispatched',
|
|
135
166
|
sessionKey: params.sessionKey,
|
|
136
167
|
runId: params.runId ?? '',
|
|
@@ -138,6 +169,9 @@ export function registerWorker(paths, params) {
|
|
|
138
169
|
unitId: params.unitId,
|
|
139
170
|
workPackage: params.workPackage,
|
|
140
171
|
reason: params.reason ?? '',
|
|
172
|
+
label: providedLabel,
|
|
173
|
+
expectedLabel,
|
|
174
|
+
projectDir: resolvedProjectDir,
|
|
141
175
|
};
|
|
142
176
|
const required = task.requiredAgents.length > 0 ? task.requiredAgents : resolveRequiredAgents(task);
|
|
143
177
|
if (!required.includes(params.agentId))
|
|
@@ -145,11 +179,37 @@ export function registerWorker(paths, params) {
|
|
|
145
179
|
task.requiredAgents = required;
|
|
146
180
|
saveTask(paths, task);
|
|
147
181
|
appendEvent(paths.eventsFile, {
|
|
148
|
-
event: 'worker_dispatched',
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
event: 'worker_dispatched',
|
|
183
|
+
taskId: params.taskId,
|
|
184
|
+
phase: 'execution',
|
|
185
|
+
actor: 'zigrix',
|
|
186
|
+
targetAgent: params.agentId,
|
|
187
|
+
status: 'IN_PROGRESS',
|
|
188
|
+
sessionKey: params.sessionKey,
|
|
189
|
+
sessionId: resolvedSessionId,
|
|
190
|
+
unitId: params.unitId,
|
|
191
|
+
workPackage: params.workPackage,
|
|
192
|
+
payload: {
|
|
193
|
+
agentId: params.agentId,
|
|
194
|
+
runId: params.runId ?? '',
|
|
195
|
+
reason: params.reason ?? '',
|
|
196
|
+
label: providedLabel,
|
|
197
|
+
projectDir: resolvedProjectDir,
|
|
198
|
+
},
|
|
151
199
|
});
|
|
152
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
taskId: params.taskId,
|
|
203
|
+
agentId: params.agentId,
|
|
204
|
+
sessionKey: params.sessionKey,
|
|
205
|
+
runId: params.runId ?? '',
|
|
206
|
+
sessionId: resolvedSessionId,
|
|
207
|
+
unitId: params.unitId,
|
|
208
|
+
workPackage: params.workPackage,
|
|
209
|
+
label: providedLabel,
|
|
210
|
+
projectDir: resolvedProjectDir,
|
|
211
|
+
status: 'dispatched',
|
|
212
|
+
};
|
|
153
213
|
}
|
|
154
214
|
export function completeWorker(paths, params) {
|
|
155
215
|
const task = loadTask(paths, params.taskId);
|
|
@@ -167,16 +227,36 @@ export function completeWorker(paths, params) {
|
|
|
167
227
|
};
|
|
168
228
|
saveTask(paths, task);
|
|
169
229
|
appendEvent(paths.eventsFile, {
|
|
170
|
-
event: 'worker_done',
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
event: 'worker_done',
|
|
231
|
+
taskId: params.taskId,
|
|
232
|
+
phase: 'execution',
|
|
233
|
+
actor: params.agentId,
|
|
234
|
+
targetAgent: params.agentId,
|
|
235
|
+
status: (params.result ?? 'done') === 'blocked' ? 'BLOCKED' : 'IN_PROGRESS',
|
|
236
|
+
sessionKey: params.sessionKey,
|
|
237
|
+
sessionId: params.sessionId ?? null,
|
|
238
|
+
unitId: params.unitId ?? prev.unitId,
|
|
239
|
+
workPackage: params.workPackage ?? prev.workPackage,
|
|
173
240
|
payload: { result: params.result ?? 'done', runId: params.runId },
|
|
174
241
|
});
|
|
175
242
|
const evidenceDir = path.join(paths.evidenceDir, params.taskId);
|
|
176
243
|
const presentAgents = fs.existsSync(evidenceDir)
|
|
177
|
-
? fs
|
|
244
|
+
? fs
|
|
245
|
+
.readdirSync(evidenceDir)
|
|
246
|
+
.filter((file) => file.endsWith('.json') && file !== '_merged.json')
|
|
247
|
+
.map((file) => path.basename(file, '.json'))
|
|
248
|
+
.sort()
|
|
178
249
|
: [];
|
|
179
250
|
const required = resolveRequiredAgents(task);
|
|
180
251
|
const missingAgents = required.filter((agentId) => !presentAgents.includes(agentId));
|
|
181
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
taskId: params.taskId,
|
|
255
|
+
agentId: params.agentId,
|
|
256
|
+
result: params.result ?? 'done',
|
|
257
|
+
requiredAgents: required,
|
|
258
|
+
presentEvidenceAgents: presentAgents,
|
|
259
|
+
missingEvidenceAgents: missingAgents,
|
|
260
|
+
allEvidenceCollected: missingAgents.length === 0,
|
|
261
|
+
};
|
|
182
262
|
}
|
package/dist/state/tasks.d.ts
CHANGED
|
@@ -91,6 +91,12 @@ export declare function createTask(paths: ZigrixPaths, params: {
|
|
|
91
91
|
prefix?: string;
|
|
92
92
|
}): ZigrixTask;
|
|
93
93
|
export declare function updateTaskStatus(paths: ZigrixPaths, taskId: string, status: string): ZigrixTask | null;
|
|
94
|
+
export declare function bindOrchestratorSession(paths: ZigrixPaths, params: {
|
|
95
|
+
taskId: string;
|
|
96
|
+
agentId: string;
|
|
97
|
+
sessionKey: string;
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
}): Record<string, unknown> | null;
|
|
94
100
|
export declare function recordTaskProgress(paths: ZigrixPaths, params: {
|
|
95
101
|
taskId: string;
|
|
96
102
|
actor: string;
|
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)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zigrix",
|
|
3
|
-
"version": "0.2.
|
|
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 통과 확인 후 전담한다.
|