cli-jaw 1.2.2 → 1.2.3

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.
@@ -1,475 +1,189 @@
1
- // ─── Orchestration v2 (Plan → Phase-aware Distribute → Quality Gate Review) ──
1
+ // ─── PABCD Orchestration ────────────────────────────
2
+ // Old round-loop pipeline fully removed.
3
+ // PABCD state machine is the sole orchestration system.
2
4
  import { broadcast } from '../core/bus.js';
3
- import { insertMessage, getEmployees, clearAllEmployeeSessions, } from '../core/db.js';
5
+ import { getEmployees, clearAllEmployeeSessions, upsertEmployeeSession, } from '../core/db.js';
4
6
  import { clearPromptCache } from '../prompt/builder.js';
5
7
  import { spawnAgent } from '../agent/spawn.js';
6
- import { createWorklog, readLatestWorklog, appendToWorklog, updateMatrix, updateWorklogStatus, parseWorklogPending } from '../memory/worklog.js';
7
- import { PHASES, findEmployee, validateParallelSafety, runSingleAgent, buildPlanPrompt, } from './distribute.js';
8
- const MAX_ROUNDS = 3;
9
- // ─── Parsing/Triage (extracted to orchestrator-parser.js) ──
10
- import { isContinueIntent, isResetIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON, parseVerdicts, } from './parser.js';
11
- export { isContinueIntent, isResetIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON };
12
- // ─── Phase 정의 (constants in distribute.ts) ─────────
13
- const PHASE_PROFILES = {
14
- frontend: [1, 2, 3, 4, 5],
15
- backend: [1, 2, 3, 4, 5],
16
- data: [1, 2, 3, 4, 5],
17
- docs: [1, 3, 5],
18
- custom: [3],
8
+ import { readLatestWorklog, appendToWorklog, updateWorklogStatus, } from '../memory/worklog.js';
9
+ import { findEmployee, runSingleAgent } from './distribute.js';
10
+ import { getState, getPrefix, resetState, setState, getStatePrompt, } from './state-machine.js';
11
+ // ─── Parser re-exports ─────────────────────────────
12
+ import { isContinueIntent, isResetIntent, isApproveIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON, } from './parser.js';
13
+ export { isContinueIntent, isResetIntent, isApproveIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON, };
14
+ const PABCD_ACTIVATE_PATTERNS = [
15
+ /^\/?orchestrate$/i,
16
+ /^\/?pabcd$/i,
17
+ /^지휘\s*모드$/i,
18
+ /^오케스트레이션(?:\s*모드)?$/i,
19
+ /^orchestration(?:\s*mode)?$/i,
20
+ ];
21
+ const AUTO_APPROVE_NEXT = {
22
+ P: 'A',
23
+ A: 'B',
24
+ B: 'C',
19
25
  };
20
- // PHASE_INSTRUCTIONS moved to distribute.ts
21
- // ─── Per-Agent Phase Tracking ────────────────────────
22
- export function initAgentPhases(subtasks) {
23
- return subtasks.map((st) => {
24
- const role = (st.role || 'custom').toLowerCase();
25
- const fullProfile = PHASE_PROFILES[role] || [3];
26
- // start_phase / end_phase 지원: planning agent가 지정한 범위
27
- // 잘못된 값은 profile 범위 내로 보정 (예: 99 -> 마지막 phase)
28
- const rawStart = Number(st.start_phase);
29
- const rawEnd = Number(st.end_phase);
30
- const minPhase = fullProfile[0];
31
- const maxPhase = fullProfile[fullProfile.length - 1];
32
- const startPhase = Number.isFinite(rawStart)
33
- ? Math.max(minPhase, Math.min(maxPhase, rawStart))
34
- : minPhase;
35
- const endPhase = Number.isFinite(rawEnd)
36
- ? Math.max(startPhase, Math.min(maxPhase, rawEnd))
37
- : maxPhase;
38
- const profile = fullProfile.filter((p) => p >= startPhase && p <= endPhase);
39
- // sparse fallback: 빈 profile이면 startPhase 이상 가장 가까운 phase 사용
40
- const effectiveProfile = profile.length > 0
41
- ? profile
42
- : [fullProfile.find((p) => p >= startPhase) || fullProfile[fullProfile.length - 1]];
43
- if (profile.length === 0) {
44
- console.warn(`[jaw:phase] ${st.agent}: no phases in [${startPhase},${endPhase}], fallback to [${effectiveProfile[0]}]`);
45
- }
46
- if (startPhase > minPhase) {
47
- console.log(`[jaw:phase-skip] ${st.agent} (${role}): skipping to phase ${startPhase}`);
48
- }
49
- return {
50
- agent: st.agent,
51
- task: st.task,
52
- role,
53
- parallel: st.parallel === true,
54
- checkpoint: st.checkpoint === true,
55
- checkpointed: false,
56
- verification: st.verification || null,
57
- phaseProfile: effectiveProfile,
58
- currentPhaseIdx: 0,
59
- currentPhase: effectiveProfile[0],
60
- completed: false,
61
- history: [],
62
- };
63
- });
64
- }
65
- function advancePhase(ap, passed) {
66
- if (!passed)
67
- return;
68
- if (ap.currentPhaseIdx < ap.phaseProfile.length - 1) {
69
- ap.currentPhaseIdx++;
70
- ap.currentPhase = ap.phaseProfile[ap.currentPhaseIdx];
71
- }
72
- else {
73
- ap.completed = true;
74
- }
26
+ function shouldAutoActivatePABCD(prompt, meta) {
27
+ const t = String(prompt || '').trim();
28
+ if (!t)
29
+ return false;
30
+ if (meta._workerResult || meta._skipAutoP)
31
+ return false;
32
+ // Only activate via explicit trigger words NOT auto for every complex message.
33
+ return PABCD_ACTIVATE_PATTERNS.some(re => re.test(t));
75
34
  }
76
- // ─── Plan Phase ──────────────────────────────────────
77
- async function phasePlan(prompt, worklog, meta = {}) {
78
- broadcast('agent_status', { agentId: 'planning', agentName: '🎯 기획', status: 'planning' });
79
- const emps = getEmployees.all();
80
- const planPrompt = buildPlanPrompt(prompt, worklog.path, emps);
81
- const { promise } = spawnAgent(planPrompt, { agentId: 'planning', _skipInsert: true, origin: meta.origin || 'web' });
82
- const result = await promise;
83
- // Agent 자율 판단: direct_answer가 있으면 subtask 생략
84
- const directAnswer = parseDirectAnswer(result.text);
85
- if (directAnswer) {
86
- return { planText: directAnswer, subtasks: [], directAnswer };
87
- }
88
- const planText = stripSubtaskJSON(result.text);
89
- appendToWorklog(worklog.path, 'Plan', planText || '(Plan Agent 응답 없음)');
90
- const subtasks = parseSubtasks(result.text);
91
- // §7.4: Fallback — if planning agent responded without JSON, treat as direct answer
92
- if (!subtasks || subtasks.length === 0) {
93
- console.warn('[orchestrator:plan] No JSON block found in planning response. Treating as direct answer.');
94
- return { planText: result.text, subtasks: [], directAnswer: result.text };
95
- }
96
- return { planText, subtasks };
97
- }
98
- // ─── Distribute Phase (per-agent phase-aware) ────────
99
- // Helper functions (buildParallelContext, buildSequentialContext, findEmployee,
100
- // validateParallelSafety, runSingleAgent) extracted to distribute.ts
101
- async function distributeByPhase(agentPhases, worklog, round, meta = {}) {
102
- const emps = getEmployees.all();
103
- const results = [];
104
- const active = agentPhases.filter((ap) => !ap.completed);
105
- if (active.length === 0)
106
- return results;
107
- // §7.3: Validate parallel safety before execution
108
- validateParallelSafety(active);
109
- const parallelGroup = active.filter(ap => ap.parallel === true);
110
- const sequentialGroup = active.filter(ap => ap.parallel !== true);
111
- // Phase 1: Run parallel group concurrently
112
- if (parallelGroup.length > 0) {
113
- console.log(`[orchestrator:parallel] Running ${parallelGroup.length} agents concurrently: ${parallelGroup.map(a => a.agent).join(', ')}`);
114
- const parallelPeers = parallelGroup.map(ap => ({
115
- agent: ap.agent, role: ap.role, verification: ap.verification,
116
- }));
117
- const parallelPromises = parallelGroup.map(ap => {
118
- const emp = findEmployee(emps, ap);
119
- if (!emp)
120
- return Promise.resolve({ agent: ap.agent, role: ap.role, status: 'skipped', text: 'Agent not found' });
121
- return runSingleAgent(ap, emp, worklog, round, meta, [], parallelPeers);
122
- });
123
- const parallelResults = await Promise.all(parallelPromises);
124
- results.push(...parallelResults);
125
- }
126
- // Phase 2: Run sequential group one-by-one (sees parallel results as prior)
127
- for (const ap of sequentialGroup) {
128
- const emp = findEmployee(emps, ap);
129
- if (!emp) {
130
- results.push({ agent: ap.agent, role: ap.role, status: 'skipped', text: 'Agent not found' });
131
- continue;
132
- }
133
- const result = await runSingleAgent(ap, emp, worklog, round, meta, results);
134
- results.push(result);
135
- }
136
- return results;
137
- }
138
- // ─── Review Phase (per-agent verdict) ────────────────
139
- async function phaseReview(results, agentPhases, worklog, round, meta = {}) {
140
- const report = results.map((r) => `- **${r.agent}** (${r.role}, ${r.phaseLabel}): ${r.status === 'done' ? '✅' : '❌'}\n ${r.text.slice(0, 1200)}`).join('\n');
141
- const matrixStr = agentPhases.map((ap) => {
142
- const base = `- ${ap.agent}: role=${ap.role}, phase=${ap.currentPhase}(${PHASES[ap.currentPhase]}), completed=${ap.completed}`;
143
- if (ap.verification) {
144
- return `${base}\n pass_criteria: ${ap.verification.pass_criteria || 'N/A'}\n fail_criteria: ${ap.verification.fail_criteria || 'N/A'}`;
145
- }
146
- return base;
147
- }).join('\n');
148
- const reviewPrompt = `## 라운드 ${round} 결과 리뷰
149
-
150
- ### 실행 결과
151
- ${report}
152
-
153
- ### 현재 Agent 상태
154
- ${matrixStr}
155
-
156
- ### Worklog
157
- ${worklog.path} — 이 파일의 변경사항도 확인하세요.
158
-
159
- ## 판정 (각 agent별로 개별 판정)
160
-
161
- ### Quality Gate 루브릭
162
- 각 agent의 현재 phase에 따라 아래 기준으로 판정:
163
-
164
- - **Phase 1 (기획)**: 영향 범위 분석 + 의존성 확인 + 엣지 케이스 목록 있는가?
165
- - **Phase 2 (기획검증)**: 실제 코드와 대조 확인 + 충돌 검사 + 테스트 전략 수립됐는가?
166
- - **Phase 3 (개발)**: 변경 파일 목록 + export/import 무결성 + 빌드 에러 없는가?
167
- - **Phase 4 (디버깅)**: 실행 결과 증거 + 버그 수정 내역 + 엣지 케이스 테스트 결과 있는가?
168
- - **Phase 5 (통합검증)**: 통합 테스트 + 문서 업데이트 + 워크플로우 동작 확인?
169
-
170
- ### 판정 규칙
171
- - **PASS**: 해당 phase의 필수 항목 모두 충족. 구체적 근거 제시.
172
- - **FAIL**: 필수 항목 중 하나라도 미충족. **구체적 수정 지시** 제공 ("더 노력하세요" 금지, 구체적 행동 제시).
173
-
174
- ### allDone 조기 완료 규칙
175
- - 모든 agent가 마지막 phase를 PASS하면 당연히 allDone: true.
176
- - **조기 완료 가능**: 커밋+테스트+푸시 완료 → 남은 phase가 있어도 allDone: true.
177
- - 판단 기준: 사용자의 원래 요청이 충족되었는가? 남은 phase가 실질적 가치를 추가하는가?
178
-
179
- JSON으로 출력:
180
- \`\`\`json
181
- {
182
- "verdicts": [
183
- { "agent": "이름", "pass": true, "feedback": "통과 근거: ..." },
184
- { "agent": "이름", "pass": false, "feedback": "수정 필요: 1. ... 2. ..." }
185
- ],
186
- "allDone": false
187
- }
188
- \`\`\`
189
-
190
- 모든 작업이 완료되면 allDone: true + 사용자에게 보여줄 자연어 요약을 함께 작성.`;
191
- broadcast('agent_status', { agentId: 'planning', agentName: '🎯 기획', status: 'reviewing' });
192
- const { promise } = spawnAgent(reviewPrompt, { agentId: 'planning', internal: true, origin: meta.origin || 'web' });
193
- const evalR = await promise;
194
- const verdicts = parseVerdicts(evalR.text);
195
- return { verdicts, rawText: evalR.text };
196
- }
197
- // ─── Main Orchestrate v2 ─────────────────────────────
35
+ // ─── orchestrate (PABCD sole entry point) ───────────
198
36
  export async function orchestrate(prompt, meta = {}) {
199
- if (!meta._skipClear)
200
- clearAllEmployeeSessions.run();
201
- clearPromptCache();
202
37
  const origin = meta.origin || 'web';
203
38
  const chatId = meta.chatId;
204
- const employees = getEmployees.all();
205
- // Triage: 간단한 메시지는 직접 응답
206
- if (employees.length > 0 && !needsOrchestration(prompt)) {
207
- console.log(`[jaw:triage] direct response (no orchestration needed)`);
208
- const { promise } = spawnAgent(prompt, { origin, _skipInsert: !!meta._skipInsert });
209
- const result = await promise;
210
- const lateSubtasks = parseSubtasks(result.text);
211
- if (lateSubtasks?.length) {
212
- console.log(`[jaw:triage] agent chose to dispatch (${lateSubtasks.length} subtasks)`);
213
- const worklog = createWorklog(prompt);
214
- broadcast('worklog_created', { path: worklog.path });
215
- if (!meta._skipClear)
216
- clearAllEmployeeSessions.run();
217
- const planText = stripSubtaskJSON(result.text);
218
- appendToWorklog(worklog.path, 'Plan', planText || '(Agent-initiated dispatch)');
219
- const agentPhases = initAgentPhases(lateSubtasks);
220
- updateMatrix(worklog.path, agentPhases);
221
- // Round loop (same as L508-553)
222
- for (let round = 1; round <= MAX_ROUNDS; round++) {
223
- updateWorklogStatus(worklog.path, 'round_' + round, round);
224
- broadcast('round_start', { round, agentPhases });
225
- const results = await distributeByPhase(agentPhases, worklog, round, { origin });
226
- let { verdicts, rawText } = await phaseReview(results, agentPhases, worklog, round, { origin });
227
- if (verdicts?.verdicts) {
228
- for (const v of verdicts.verdicts) {
229
- const ap = agentPhases.find((a) => a.agent === v.agent);
230
- if (ap) {
231
- const judgedPhase = ap.currentPhase;
232
- advancePhase(ap, v.pass);
233
- ap.history.push({ round, phase: judgedPhase, pass: v.pass, feedback: v.feedback });
234
- }
235
- }
236
- }
237
- else {
238
- // Retry once — DIFF-C의 절삭 확대 덕분에 재시도 성공 확률 향상
239
- console.warn(`[jaw:review] verdict parse failed — retrying once (round ${round})`);
240
- const retryResult = await phaseReview(results, agentPhases, worklog, round, { origin });
241
- if (retryResult.verdicts?.verdicts) {
242
- verdicts = retryResult.verdicts;
243
- rawText = retryResult.rawText;
244
- for (const v of retryResult.verdicts.verdicts) {
245
- const ap = agentPhases.find((a) => a.agent === v.agent);
246
- if (ap) {
247
- const judgedPhase = ap.currentPhase;
248
- advancePhase(ap, v.pass);
249
- ap.history.push({ round, phase: judgedPhase, pass: v.pass, feedback: v.feedback });
250
- }
251
- }
252
- }
253
- else {
254
- console.error(`[jaw:review] verdict parse failed after retry — all active marked FAIL (round ${round})`);
255
- for (const ap of agentPhases.filter((a) => !a.completed)) {
256
- ap.history.push({ round, phase: ap.currentPhase, pass: false, feedback: 'auto-fail (verdict parse failed x2)' });
257
- }
258
- }
259
- }
260
- updateMatrix(worklog.path, agentPhases);
261
- // 완료 판정
262
- const scopeDone = agentPhases.every((ap) => ap.completed)
263
- || verdicts?.allDone === true;
264
- const hasCheckpoint = agentPhases.some((ap) => ap.checkpoint && !ap.checkpointed);
265
- if (scopeDone && hasCheckpoint) {
266
- // CHECKPOINT: completed 되돌리기 + 세션 보존 (advancePhase가 completed=true 찍었으므로 reset)
267
- agentPhases.forEach((ap) => {
268
- if (ap.checkpoint) {
269
- ap.checkpointed = true;
270
- ap.completed = false; // resume 가능하게 되돌리기
271
- }
272
- });
273
- updateMatrix(worklog.path, agentPhases);
274
- const summary = stripSubtaskJSON(rawText) || '요청된 scope 완료';
275
- appendToWorklog(worklog.path, 'Final Summary', summary);
276
- updateWorklogStatus(worklog.path, 'checkpoint', round);
277
- insertMessage.run('assistant', summary + '\n\n다음: "리뷰해봐", "이어서 해줘", "리셋해"', 'orchestrator', '');
278
- broadcast('orchestrate_done', { text: summary, worklog: worklog.path, origin, chatId, checkpoint: true });
279
- return;
280
- }
281
- if (scopeDone) {
282
- // DONE: 진짜 완료
283
- agentPhases.forEach((ap) => { ap.completed = true; });
284
- updateMatrix(worklog.path, agentPhases);
285
- const summary = stripSubtaskJSON(rawText) || '모든 작업 완료';
286
- appendToWorklog(worklog.path, 'Final Summary', summary);
287
- updateWorklogStatus(worklog.path, 'done', round);
288
- clearAllEmployeeSessions.run();
289
- insertMessage.run('assistant', summary, 'orchestrator', '');
290
- broadcast('orchestrate_done', { text: summary, worklog: worklog.path, origin, chatId });
291
- return;
292
- }
293
- broadcast('round_done', { round, action: 'next', agentPhases });
294
- if (round === MAX_ROUNDS) {
295
- const done = agentPhases.filter((ap) => ap.completed);
296
- const pending = agentPhases.filter((ap) => !ap.completed);
297
- const partial = `## 완료 (${done.length})\n${done.map((a) => `- ✅ ${a.agent} (${a.role})`).join('\n')}\n\n` +
298
- `## 미완료 (${pending.length})\n${pending.map((a) => `- ⏳ ${a.agent} (${a.role}) — Phase ${a.currentPhase}: ${PHASES[a.currentPhase]}`).join('\n')}\n\n` +
299
- `이어서 진행하려면 "이어서 해줘"라고 말씀하세요.\nWorklog: ${worklog.path}`;
300
- appendToWorklog(worklog.path, 'Final Summary', partial);
301
- updateWorklogStatus(worklog.path, 'partial', round);
302
- // partial: 세션 보존 (이어서 해줘 대비, 새 orchestrate() 시 L228에서 자동 정리)
303
- insertMessage.run('assistant', partial, 'orchestrator', '');
304
- broadcast('orchestrate_done', { text: partial, worklog: worklog.path, origin, chatId });
305
- }
306
- }
307
- return;
308
- }
309
- const stripped = stripSubtaskJSON(result.text);
310
- broadcast('orchestrate_done', { text: stripped || result.text || '', origin, chatId });
311
- return;
312
- }
313
- // 직원 없으면 단일 에이전트 모드
314
- if (employees.length === 0) {
315
- const { promise } = spawnAgent(prompt, { origin, _skipInsert: !!meta._skipInsert });
316
- const result = await promise;
317
- const stripped = stripSubtaskJSON(result.text);
318
- broadcast('orchestrate_done', { text: stripped || result.text || '', origin, chatId });
319
- return;
320
- }
321
- const worklog = createWorklog(prompt);
322
- broadcast('worklog_created', { path: worklog.path });
323
- if (!meta._skipClear)
39
+ const userText = String(prompt || '').trim();
40
+ let state = getState();
41
+ let skipPrefix = !!meta._skipPrefix;
42
+ // Skip session clear during active PABCD (preserve resume)
43
+ if (!meta._skipClear && state === 'IDLE') {
324
44
  clearAllEmployeeSessions.run();
325
- // 1. 기획 (planning agent가 직접 응답할 수도 있음)
326
- const { planText, subtasks, directAnswer } = await phasePlan(prompt, worklog, { origin });
327
- // Agent 자율 판단: subtask 불필요 → 직접 응답
328
- if (directAnswer) {
329
- console.log('[jaw:triage] planning agent chose direct response');
330
- broadcast('agent_done', { text: directAnswer, origin });
331
- broadcast('orchestrate_done', { text: directAnswer, origin, chatId });
332
- return;
333
45
  }
334
- if (!subtasks?.length) {
335
- broadcast('orchestrate_done', { text: planText || '', origin, chatId });
336
- return;
46
+ // Auto-enter P mode from IDLE for orchestration-worthy tasks.
47
+ if (state === 'IDLE' && shouldAutoActivatePABCD(userText, meta)) {
48
+ setState('P', {
49
+ originalPrompt: userText || prompt,
50
+ plan: null,
51
+ workerResults: [],
52
+ origin,
53
+ chatId,
54
+ });
55
+ state = 'P';
56
+ prompt = `${getStatePrompt('P')}\n\nUser request:\n${userText || prompt}`;
57
+ skipPrefix = true;
58
+ console.log('[jaw:pabcd] auto-transition IDLE -> P');
337
59
  }
338
- // 2. Per-agent phase 초기화
339
- const agentPhases = initAgentPhases(subtasks);
340
- updateMatrix(worklog.path, agentPhases);
341
- // 3. Round loop
342
- for (let round = 1; round <= MAX_ROUNDS; round++) {
343
- updateWorklogStatus(worklog.path, 'round_' + round, round);
344
- broadcast('round_start', { round, agentPhases });
345
- const results = await distributeByPhase(agentPhases, worklog, round, { origin });
346
- let { verdicts, rawText } = await phaseReview(results, agentPhases, worklog, round, { origin });
347
- // 4. Per-agent phase advance
348
- if (verdicts?.verdicts) {
349
- for (const v of verdicts.verdicts) {
350
- const ap = agentPhases.find((a) => a.agent === v.agent);
351
- if (ap) {
352
- const judgedPhase = ap.currentPhase; // advance 전 기록
353
- advancePhase(ap, v.pass);
354
- ap.history.push({ round, phase: judgedPhase, pass: v.pass, feedback: v.feedback });
355
- }
356
- }
60
+ // Auto-advance by explicit approval intent (no shell command dependency).
61
+ if (state !== 'IDLE' && !meta._workerResult && isApproveIntent(userText)) {
62
+ const next = AUTO_APPROVE_NEXT[state];
63
+ if (next) {
64
+ const prev = state;
65
+ setState(next);
66
+ state = next;
67
+ prompt = `${getStatePrompt(next)}\n\nUser approval:\n${userText}`;
68
+ skipPrefix = true;
69
+ console.log(`[jaw:pabcd] auto-transition ${prev} -> ${next} (approve intent)`);
357
70
  }
358
- else {
359
- // Retry once — DIFF-C의 절삭 확대 덕분에 재시도 성공 확률 향상
360
- console.warn(`[jaw:review] verdict parse failed — retrying once (round ${round})`);
361
- const retryResult = await phaseReview(results, agentPhases, worklog, round, { origin });
362
- if (retryResult.verdicts?.verdicts) {
363
- verdicts = retryResult.verdicts;
364
- rawText = retryResult.rawText;
365
- for (const v of retryResult.verdicts.verdicts) {
366
- const ap = agentPhases.find((a) => a.agent === v.agent);
367
- if (ap) {
368
- const judgedPhase = ap.currentPhase;
369
- advancePhase(ap, v.pass);
370
- ap.history.push({ round, phase: judgedPhase, pass: v.pass, feedback: v.feedback });
371
- }
372
- }
373
- }
374
- else {
375
- console.error(`[jaw:review] verdict parse failed after retry — all active marked FAIL (round ${round})`);
376
- for (const ap of agentPhases.filter((a) => !a.completed)) {
377
- ap.history.push({ round, phase: ap.currentPhase, pass: false, feedback: 'auto-fail (verdict parse failed x2)' });
378
- }
71
+ }
72
+ clearPromptCache();
73
+ // prefix injection
74
+ const source = meta._workerResult ? 'worker' : 'user';
75
+ const prefix = getPrefix(state, source);
76
+ if (prefix && !skipPrefix) {
77
+ prompt = prefix + '\n' + prompt;
78
+ }
79
+ // spawn/resume agent
80
+ console.log(`[jaw:pabcd] state=${state}, spawning/resuming agent`);
81
+ const { promise } = spawnAgent(prompt, {
82
+ origin,
83
+ _skipInsert: !!meta._skipInsert,
84
+ });
85
+ const result = await promise;
86
+ // Worker JSON detected → spawn workers → feed results back
87
+ const workerTasks = parseSubtasks(result.text);
88
+ if (workerTasks?.length && state !== 'IDLE') {
89
+ // Only dispatch workers during active PABCD (P/A/B/C).
90
+ // In IDLE, subtask JSON is handled by the middleware, not here.
91
+ console.log(`[jaw:pabcd] worker JSON detected (${workerTasks.length} tasks)`);
92
+ // Map PABCD state → worker phase context
93
+ const PABCD_PHASE_MAP = { A: 2, B: 3, C: 4 };
94
+ const workerPhase = PABCD_PHASE_MAP[state] || 3;
95
+ let anyWorkerRan = false;
96
+ for (const wt of workerTasks) {
97
+ const emp = findEmployee(getEmployees.all(), wt);
98
+ if (!emp) {
99
+ console.warn(`[jaw:pabcd] worker not found: ${wt.agent}`);
100
+ continue;
379
101
  }
380
- }
381
- updateMatrix(worklog.path, agentPhases);
382
- // 5. 완료 판정
383
- const scopeDone = agentPhases.every((ap) => ap.completed)
384
- || verdicts?.allDone === true;
385
- const hasCheckpoint = agentPhases.some((ap) => ap.checkpoint && !ap.checkpointed);
386
- if (scopeDone && hasCheckpoint) {
387
- // CHECKPOINT: completed 되돌리기 + 세션 보존 (advancePhase가 completed=true 찍었으므로 reset)
388
- agentPhases.forEach((ap) => {
389
- if (ap.checkpoint) {
390
- ap.checkpointed = true;
391
- ap.completed = false; // resume 가능하게 되돌리기
392
- }
102
+ // Force fresh session for PABCD workers (prevent context contamination)
103
+ upsertEmployeeSession.run(emp.id, null, emp.cli);
104
+ const wResult = await runSingleAgent({
105
+ ...wt,
106
+ phaseProfile: [workerPhase],
107
+ currentPhaseIdx: 0,
108
+ currentPhase: workerPhase,
109
+ completed: false,
110
+ history: [],
111
+ }, emp, { path: '' }, 1, { origin }, []);
112
+ anyWorkerRan = true;
113
+ // Feed worker results back to the main agent
114
+ await orchestrate(wResult.text, {
115
+ ...meta,
116
+ _skipClear: true,
117
+ _workerResult: true,
393
118
  });
394
- updateMatrix(worklog.path, agentPhases);
395
- const summary = stripSubtaskJSON(rawText) || '요청된 scope 완료';
396
- appendToWorklog(worklog.path, 'Final Summary', summary);
397
- updateWorklogStatus(worklog.path, 'checkpoint', round);
398
- insertMessage.run('assistant', summary + '\n\n다음: "리뷰해봐", "이어서 해줘", "리셋해"', 'orchestrator', '');
399
- broadcast('orchestrate_done', { text: summary, worklog: worklog.path, origin, checkpoint: true });
400
- break;
401
- }
402
- if (scopeDone) {
403
- // DONE: 진짜 완료
404
- agentPhases.forEach((ap) => { ap.completed = true; });
405
- updateMatrix(worklog.path, agentPhases);
406
- const summary = stripSubtaskJSON(rawText) || '모든 작업 완료';
407
- appendToWorklog(worklog.path, 'Final Summary', summary);
408
- updateWorklogStatus(worklog.path, 'done', round);
409
- clearAllEmployeeSessions.run();
410
- insertMessage.run('assistant', summary, 'orchestrator', '');
411
- broadcast('orchestrate_done', { text: summary, worklog: worklog.path, origin, chatId });
412
- break;
413
119
  }
414
- broadcast('round_done', { round, action: 'next', agentPhases });
415
- // 6. Max round 도달 → 부분 보고
416
- if (round === MAX_ROUNDS) {
417
- const done = agentPhases.filter((ap) => ap.completed);
418
- const pending = agentPhases.filter((ap) => !ap.completed);
419
- const partial = `## 완료 (${done.length})\n${done.map((a) => `- ✅ ${a.agent} (${a.role})`).join('\n')}\n\n` +
420
- `## 미완료 (${pending.length})\n${pending.map((a) => `- ⏳ ${a.agent} (${a.role}) — Phase ${a.currentPhase}: ${PHASES[a.currentPhase]}`).join('\n')}\n\n` +
421
- `이어서 진행하려면 "이어서 해줘"라고 말씀하세요.\nWorklog: ${worklog.path}`;
422
- appendToWorklog(worklog.path, 'Final Summary', partial);
423
- updateWorklogStatus(worklog.path, 'partial', round);
424
- // partial: 세션 보존 (이어서 해줘 대비, 새 orchestrate() 시 L228에서 자동 정리)
425
- insertMessage.run('assistant', partial, 'orchestrator', '');
426
- broadcast('orchestrate_done', { text: partial, worklog: worklog.path, origin, chatId });
120
+ // If no workers could be found, broadcast the original response
121
+ if (!anyWorkerRan) {
122
+ const stripped = stripSubtaskJSON(result.text);
123
+ broadcast('orchestrate_done', {
124
+ text: `[Worker dispatch failed — no matching employees]\n${stripped || result.text || ''}`,
125
+ origin,
126
+ chatId,
127
+ });
427
128
  }
129
+ return;
428
130
  }
131
+ // Normal response → broadcast
132
+ const stripped = stripSubtaskJSON(result.text);
133
+ broadcast('orchestrate_done', {
134
+ text: stripped || result.text || '',
135
+ origin,
136
+ chatId,
137
+ });
429
138
  }
430
- // ─── Continue (이어서 해줘) ───────────────────────────
139
+ // ─── Continue ───────────────────────────────────────
431
140
  export async function orchestrateContinue(meta = {}) {
432
141
  const origin = meta.origin || 'web';
433
142
  const chatId = meta.chatId;
434
- const latest = readLatestWorklog();
435
- if (!latest || latest.content.includes('Status: done') || latest.content.includes('Status: reset')) {
436
- broadcast('orchestrate_done', { text: '이어갈 worklog가 없습니다.', origin, chatId });
437
- return;
143
+ const state = getState();
144
+ // Active PABCD resume from current state
145
+ if (state !== 'IDLE') {
146
+ console.log(`[jaw:pabcd] continue in state=${state}`);
147
+ return orchestrate('Please continue from where you left off.', {
148
+ ...meta,
149
+ _skipClear: true,
150
+ });
438
151
  }
439
- const pending = parseWorklogPending(latest.content);
440
- if (!pending.length) {
441
- broadcast('orchestrate_done', { text: '모든 작업이 이미 완료되었습니다.', origin, chatId });
152
+ // IDLE + incomplete worklog → worklog-based resume
153
+ const latest = readLatestWorklog();
154
+ if (!latest ||
155
+ latest.content.includes('Status: done') ||
156
+ latest.content.includes('Status: reset')) {
157
+ broadcast('orchestrate_done', {
158
+ text: 'No pending work to continue.',
159
+ origin,
160
+ chatId,
161
+ });
442
162
  return;
443
163
  }
444
- const resumePrompt = `## 이어서 작업
445
- 이전 worklog를 읽고 미완료 항목을 이어서 진행하세요.
446
-
447
- Worklog: ${latest.path}
448
-
449
- 미완료 항목:
450
- ${pending.map((p) => `- ${p.agent} (${p.role}): Phase ${p.currentPhase}`).join('\n')}
451
-
452
- ## 제약 조건
453
- - 위 미완료 항목의 **agent 이름, role, 현재 phase를 그대로 유지**하세요.
454
- - 새로운 agent를 추가하거나 role을 변경하지 마세요.
455
- - task 내용만 현재 phase에 맞게 구체화하세요.
456
- - start_phase는 각 agent의 현재 phase와 동일하게 설정하세요.
457
-
458
- subtask JSON을 출력하세요.`;
459
- return orchestrate(resumePrompt, { ...meta, _skipClear: true });
164
+ return orchestrate(`Read the previous worklog and continue any incomplete tasks.\nWorklog: ${latest.path}`, { ...meta, _skipClear: true });
460
165
  }
461
- // ─── Reset (리셋해) ───────────────────────────────────
166
+ // ─── Reset ──────────────────────────────────────────
462
167
  export async function orchestrateReset(meta = {}) {
463
168
  const origin = meta.origin || 'web';
464
169
  const chatId = meta.chatId;
465
170
  clearAllEmployeeSessions.run();
171
+ resetState();
466
172
  const latest = readLatestWorklog();
467
173
  if (!latest) {
468
- broadcast('orchestrate_done', { text: '리셋할 worklog가 없습니다.', origin, chatId });
174
+ broadcast('orchestrate_done', {
175
+ text: 'Reset complete.',
176
+ origin,
177
+ chatId,
178
+ });
469
179
  return;
470
180
  }
471
181
  updateWorklogStatus(latest.path, 'reset', 0);
472
- appendToWorklog(latest.path, 'Final Summary', '유저 요청으로 리셋됨.');
473
- broadcast('orchestrate_done', { text: '리셋 완료.', origin, chatId });
182
+ appendToWorklog(latest.path, 'Final Summary', 'Reset by user request.');
183
+ broadcast('orchestrate_done', {
184
+ text: 'Reset complete.',
185
+ origin,
186
+ chatId,
187
+ });
474
188
  }
475
189
  //# sourceMappingURL=pipeline.js.map