zigrix 0.2.0 → 0.2.2

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.
Files changed (61) hide show
  1. package/README.md +10 -2
  2. package/dist/dashboard/.next/BUILD_ID +1 -1
  3. package/dist/dashboard/.next/app-build-manifest.json +22 -22
  4. package/dist/dashboard/.next/app-path-routes-manifest.json +1 -1
  5. package/dist/dashboard/.next/build-manifest.json +5 -5
  6. package/dist/dashboard/.next/prerender-manifest.json +10 -10
  7. package/dist/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/dist/dashboard/.next/server/app/_not-found.html +1 -1
  9. package/dist/dashboard/.next/server/app/_not-found.rsc +1 -1
  10. package/dist/dashboard/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  11. package/dist/dashboard/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  12. package/dist/dashboard/.next/server/app/api/auth/session/route_client-reference-manifest.js +1 -1
  13. package/dist/dashboard/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -1
  14. package/dist/dashboard/.next/server/app/api/overview/route_client-reference-manifest.js +1 -1
  15. package/dist/dashboard/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  16. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  17. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/conversation/route_client-reference-manifest.js +1 -1
  18. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  19. package/dist/dashboard/.next/server/app/login/page_client-reference-manifest.js +1 -1
  20. package/dist/dashboard/.next/server/app/login.html +1 -1
  21. package/dist/dashboard/.next/server/app/login.rsc +1 -1
  22. package/dist/dashboard/.next/server/app/page.js +2 -2
  23. package/dist/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/dist/dashboard/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  25. package/dist/dashboard/.next/server/app/setup.html +1 -1
  26. package/dist/dashboard/.next/server/app/setup.rsc +1 -1
  27. package/dist/dashboard/.next/server/app-paths-manifest.json +1 -1
  28. package/dist/dashboard/.next/server/chunks/331.js +1 -1
  29. package/dist/dashboard/.next/server/chunks/972.js +1 -1
  30. package/dist/dashboard/.next/server/functions-config-manifest.json +4 -4
  31. package/dist/dashboard/.next/server/middleware-build-manifest.js +1 -1
  32. package/dist/dashboard/.next/server/middleware.js +1 -1
  33. package/dist/dashboard/.next/server/pages/404.html +1 -1
  34. package/dist/dashboard/.next/server/pages/500.html +1 -1
  35. package/dist/dashboard/.next/server/pages/_error.js +1 -1
  36. package/dist/dashboard/.next/static/chunks/{255-ebd51be49873d76c.js → 255-4f212684648fcab9.js} +1 -1
  37. package/dist/dashboard/.next/static/chunks/app/page-0104be5a259641e4.js +1 -0
  38. package/dist/dashboard/.next/static/chunks/main-cec07dc17fdd452c.js +1 -0
  39. package/dist/dashboard/.next/static/css/{c3a7306cb2ba3f6c.css → 819f4ef2c68a9009.css} +1 -1
  40. package/dist/dashboard/package.json +2 -2
  41. package/dist/index.js +25 -2
  42. package/dist/onboard.d.ts +1 -1
  43. package/dist/onboard.js +2 -2
  44. package/dist/orchestration/dispatch.js +16 -71
  45. package/dist/orchestration/prompt-compose.d.ts +35 -0
  46. package/dist/orchestration/prompt-compose.js +172 -0
  47. package/dist/orchestration/worker.d.ts +4 -1
  48. package/dist/orchestration/worker.js +132 -52
  49. package/dist/state/tasks.d.ts +6 -0
  50. package/dist/state/tasks.js +39 -0
  51. package/package.json +3 -3
  52. package/rules/defaults/orchestrator-agent.md +4 -2
  53. package/rules/defaults/worker-common.md +3 -0
  54. package/skills/oz/SKILL.md +117 -0
  55. package/skills/oz/references/examples.md +44 -0
  56. package/skills/oz/references/routing-rubric.md +71 -0
  57. package/skills/zigrix-main-agent-guide/SKILL.md +37 -7
  58. package/dist/dashboard/.next/static/chunks/app/page-0314989c31e18b4b.js +0 -1
  59. package/dist/dashboard/.next/static/chunks/main-da2d845a416cfa3f.js +0 -1
  60. /package/dist/dashboard/.next/static/{EZjkAnODdTglaMXuBw76E → j9XNuQEcfIYIh8oRB9Yp_}/_buildManifest.js +0 -0
  61. /package/dist/dashboard/.next/static/{EZjkAnODdTglaMXuBw76E → j9XNuQEcfIYIh8oRB9Yp_}/_ssgManifest.js +0 -0
@@ -2,8 +2,9 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { ROLE_HINTS } from '../agents/roles.js';
4
4
  import { resolveAbsolutePath } from '../config/defaults.js';
5
+ import { composeOrchestratorPrompt, buildSpawnLabel } from './prompt-compose.js';
5
6
  import { appendEvent } from '../state/events.js';
6
- import { createTask, rebuildIndex, saveTask } from '../state/tasks.js';
7
+ import { createTask, rebuildIndex, resolveTaskPaths, saveTask } from '../state/tasks.js';
7
8
  import { ensureBaseState } from '../state/paths.js';
8
9
  const BASELINE_REQUIRED_ROLES = ['orchestrator', 'qa'];
9
10
  const CANDIDATE_ROLE_ORDER = ['frontend', 'backend', 'system', 'security'];
@@ -137,47 +138,6 @@ function defaultExecutionUnits(scale, owners) {
137
138
  { id: 'U3', title: 'qa / regression', kind: 'verification', owner: owners.qaAgentId, workPackage: 'verification', dependsOn: ['U2'], parallel: false, status: 'OPEN', dod: 'qa evidence attached' },
138
139
  ];
139
140
  }
140
- function buildBootPrompt(task, options) {
141
- return `## Orchestration Task Boot: ${task.taskId}
142
- - **Title:** ${task.title}
143
- - **Scale:** ${task.scale}
144
- - **Orchestrator:** ${options.orchestratorId}
145
-
146
- ---
147
-
148
- ## ⚠️ 절대 규칙: QA 역할 워커 호출 필수
149
-
150
- **이 태스크는 QA 역할(${options.qaAgentId}) 워커 완료가 필수다.**
151
-
152
- ---
153
-
154
- ## ⚡ 필수 첫 단계 (건너뛰기 금지)
155
-
156
- 아래 명령을 **가장 먼저** 실행하라:
157
-
158
- \`\`\`bash
159
- zigrix task start ${task.taskId} --json
160
- \`\`\`
161
-
162
- 그 후 태스크 메타를 확인하라:
163
-
164
- \`\`\`bash
165
- zigrix task status ${task.taskId} --json
166
- \`\`\`
167
-
168
- 워커 호출 시:
169
- \`\`\`bash
170
- zigrix worker prepare --task-id ${task.taskId} --agent-id <workerId> --description "..." --json
171
- zigrix worker register --task-id ${task.taskId} --agent-id <workerId> --session-key <key> --json
172
- zigrix worker complete --task-id ${task.taskId} --agent-id <workerId> --session-key <key> --run-id <rid> --json
173
- \`\`\`
174
-
175
- 최종 완료:
176
- \`\`\`bash
177
- zigrix task finalize ${task.taskId} --json
178
- \`\`\`
179
- `;
180
- }
181
141
  export function dispatchTask(paths, config, params) {
182
142
  ensureBaseState(paths);
183
143
  const selection = resolveAgentSelection(config, params.scale);
@@ -204,37 +164,21 @@ export function dispatchTask(paths, config, params) {
204
164
  task.orchestratorId = selection.orchestratorId;
205
165
  task.qaAgentId = selection.qaAgentId;
206
166
  saveTask(paths, task);
167
+ const taskPaths = resolveTaskPaths(paths, task.taskId);
207
168
  const promptPath = path.join(paths.promptsDir, `${task.taskId}-dispatch.md`);
208
- const dispatchPrompt = [
209
- `## Orchestration Task: ${task.taskId}`,
210
- '',
211
- '### 기본 정보',
212
- `- **Title:** ${task.title}`,
213
- `- **Scale:** ${task.scale}`,
214
- `- **Orchestrator:** ${selection.orchestratorId}`,
215
- `- **Baseline Required Agents:** ${selection.requiredAgents.join(', ')}`,
216
- `- **Candidate Agents:** ${selection.candidateAgents.length > 0 ? selection.candidateAgents.join(', ') : '(none)'}`,
217
- `- **Required Roles:** ${selection.requiredRoles.join(', ')}`,
218
- `- **Optional Roles:** ${selection.optionalRoles.length > 0 ? selection.optionalRoles.join(', ') : '(none)'}`,
219
- projectDir ? `- **Project Dir:** ${projectDir}` : '',
220
- '',
221
- '### 요청 내용',
222
- params.description,
223
- params.constraints ? `\n### 제약사항\n${params.constraints}` : '',
224
- '',
225
- '### 역할 매핑',
226
- ...Object.entries(selection.roleAgentMap)
227
- .filter(([, agentIds]) => agentIds.length > 0)
228
- .map(([role, agentIds]) => `- ${role}: ${agentIds.join(', ')}`),
229
- '',
230
- '### 선택 규칙',
231
- ...Object.entries(selection.selectionHints).map(([agentId, hint]) => `- ${agentId}: ${hint}`),
232
- ].filter(Boolean).join('\n');
233
- fs.writeFileSync(promptPath, `${dispatchPrompt}\n`, 'utf8');
234
- const orchestratorPrompt = buildBootPrompt(task, {
169
+ const orchestratorLabel = buildSpawnLabel(task.taskId, selection.orchestratorId);
170
+ const orchestratorPrompt = composeOrchestratorPrompt({
171
+ paths,
172
+ task,
235
173
  orchestratorId: selection.orchestratorId,
236
174
  qaAgentId: selection.qaAgentId,
175
+ promptPath,
176
+ specPath: taskPaths.specPath,
177
+ metaPath: taskPaths.metaPath,
178
+ projectDir: projectDir ?? null,
179
+ constraints: params.constraints,
237
180
  });
181
+ fs.writeFileSync(promptPath, `${orchestratorPrompt}\n`, 'utf8');
238
182
  appendEvent(paths.eventsFile, {
239
183
  event: 'task_dispatched',
240
184
  taskId: task.taskId,
@@ -265,10 +209,11 @@ export function dispatchTask(paths, config, params) {
265
209
  requiredRoles: selection.requiredRoles,
266
210
  optionalRoles: selection.optionalRoles,
267
211
  roleAgentMap: selection.roleAgentMap,
268
- specPath: path.join(paths.tasksDir, `${task.taskId}.md`),
269
- metaPath: path.join(paths.tasksDir, `${task.taskId}.meta.json`),
212
+ specPath: taskPaths.specPath,
213
+ metaPath: taskPaths.metaPath,
270
214
  promptPath,
271
215
  orchestratorPrompt,
216
+ orchestratorLabel,
272
217
  projectDir: projectDir ?? null,
273
218
  };
274
219
  }
@@ -0,0 +1,35 @@
1
+ import { type StandardAgentRole } from '../agents/roles.js';
2
+ import type { ZigrixConfig } from '../config/schema.js';
3
+ import { type ZigrixPaths } from '../state/paths.js';
4
+ import { type ZigrixTask } from '../state/tasks.js';
5
+ export declare function resolveAgentRole(config: ZigrixConfig, task: ZigrixTask, agentId: string): StandardAgentRole;
6
+ export declare function buildSpawnLabel(taskId: string, agentId: string): string;
7
+ export declare function composeOrchestratorPrompt(params: {
8
+ paths: ZigrixPaths;
9
+ task: ZigrixTask;
10
+ orchestratorId: string;
11
+ qaAgentId: string;
12
+ promptPath: string;
13
+ specPath: string;
14
+ metaPath: string;
15
+ projectDir?: string | null;
16
+ constraints?: string;
17
+ }): string;
18
+ export declare function composeWorkerPrompt(params: {
19
+ paths: ZigrixPaths;
20
+ config: ZigrixConfig;
21
+ task: ZigrixTask;
22
+ agentId: string;
23
+ description: string;
24
+ constraints?: string;
25
+ unitId?: string;
26
+ workPackage?: string;
27
+ dod?: string;
28
+ projectDir?: string | null;
29
+ promptPath: string;
30
+ specPath: string;
31
+ metaPath: string;
32
+ }): {
33
+ prompt: string;
34
+ role: StandardAgentRole;
35
+ };
@@ -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 renderPrompt(params) {
51
- const sections = [
52
- `## Worker Assignment: ${params.task.taskId}`,
53
- '',
54
- '| Field | Value |',
55
- '|---|---|',
56
- `| taskId | ${params.task.taskId} |`,
57
- `| title | ${params.task.title} |`,
58
- `| scale | ${params.task.scale} |`,
59
- `| role | ${params.agentId} |`,
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 projectDir = params.projectDir ?? task.projectDir;
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
- fs.writeFileSync(promptPath, `${prompt}\n`, 'utf8');
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: { agentId: params.agentId, description: params.description, constraints: params.constraints ?? '', dod: params.dod ?? '', promptPath },
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: projectDir ?? null,
115
- ...resolveTaskPaths(paths, params.taskId),
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
- // Resolve sessionId: use provided value, or fall back to parsing it from sessionKey
131
- const resolvedSessionId = params.sessionId || parseSessionIdFromKey(params.sessionKey) || null;
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
- ...(task.workerSessions[params.agentId] ?? {}),
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', taskId: params.taskId, phase: 'execution', actor: 'zigrix', targetAgent: params.agentId, status: 'IN_PROGRESS',
149
- sessionKey: params.sessionKey, sessionId: resolvedSessionId, unitId: params.unitId, workPackage: params.workPackage,
150
- payload: { agentId: params.agentId, runId: params.runId ?? '', reason: params.reason ?? '' },
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 { ok: true, taskId: params.taskId, agentId: params.agentId, sessionKey: params.sessionKey, runId: params.runId ?? '', sessionId: resolvedSessionId, unitId: params.unitId, workPackage: params.workPackage, status: 'dispatched' };
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', taskId: params.taskId, phase: 'execution', actor: params.agentId, targetAgent: params.agentId,
171
- status: (params.result ?? 'done') === 'blocked' ? 'BLOCKED' : 'IN_PROGRESS', sessionKey: params.sessionKey, sessionId: params.sessionId ?? null,
172
- unitId: params.unitId ?? prev.unitId, workPackage: params.workPackage ?? prev.workPackage,
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.readdirSync(evidenceDir).filter((file) => file.endsWith('.json') && file !== '_merged.json').map((file) => path.basename(file, '.json')).sort()
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 { ok: true, taskId: params.taskId, agentId: params.agentId, result: params.result ?? 'done', requiredAgents: required, presentEvidenceAgents: presentAgents, missingEvidenceAgents: missingAgents, allEvidenceCollected: missingAgents.length === 0 };
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
  }
@@ -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;
@@ -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)