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.
- package/dist/bin/cli-jaw.js +5 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/orchestrate.js +42 -0
- package/dist/bin/commands/orchestrate.js.map +1 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/dist/src/core/db.js +12 -0
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/orchestrator/distribute.js +64 -59
- package/dist/src/orchestrator/distribute.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +22 -7
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/parser.js +13 -0
- package/dist/src/orchestrator/parser.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +153 -439
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/state-machine.js +162 -0
- package/dist/src/orchestrator/state-machine.js.map +1 -0
- package/dist/src/prompt/builder.js +50 -37
- package/dist/src/prompt/builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,475 +1,189 @@
|
|
|
1
|
-
// ─── Orchestration
|
|
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 {
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
// ───
|
|
10
|
-
import { isContinueIntent, isResetIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON,
|
|
11
|
-
export { isContinueIntent, isResetIntent, needsOrchestration, parseSubtasks, parseDirectAnswer, stripSubtaskJSON };
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
// ───
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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', {
|
|
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', {
|
|
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
|