clideck 1.30.2 → 1.30.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/handlers.js +1 -1
- package/package.json +1 -1
- package/plugin-loader.js +1 -1
- package/plugins/autopilot/clideck-plugin.json +1 -1
- package/plugins/autopilot/index.js +511 -88
- package/plugins/autopilot/prompt.md +23 -21
- package/public/fx/bold-beep-idle.mp3 +0 -0
- package/public/fx/default-beep.mp3 +0 -0
- package/public/fx/echo-beep-idle.mp3 +0 -0
- package/public/fx/musical-beep-idle.mp3 +0 -0
- package/public/fx/small-bleep-idle.mp3 +0 -0
- package/public/fx/soft-beep.mp3 +0 -0
- package/public/fx/space-idle.mp3 +0 -0
- package/public/index.html +0 -5
- package/public/js/app.js +5 -4
- package/public/js/creator.js +45 -87
- package/public/js/nav.js +2 -2
- package/public/js/terminals.js +63 -8
- package/public/tailwind.css +1 -1
- package/transcript.js +27 -16
|
@@ -5,6 +5,12 @@ const { join } = require('path');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
|
|
7
7
|
const DATA_DIR = join(require('os').homedir(), '.clideck', 'autopilot');
|
|
8
|
+
const GOALS_DIR = join(DATA_DIR, 'goals');
|
|
9
|
+
const GOAL_PREFIX = 'AUTOPILOT GOAL:';
|
|
10
|
+
const FIRST_USER_MESSAGES = 3;
|
|
11
|
+
const FIRST_TURN_SCAN = 50;
|
|
12
|
+
const GOAL_SEARCH_TURNS = 2000;
|
|
13
|
+
const MAX_GOAL_MESSAGE_CHARS = 100000;
|
|
8
14
|
|
|
9
15
|
// --- State ---
|
|
10
16
|
const projects = new Map(); // projectId → Project
|
|
@@ -16,6 +22,7 @@ let piAi = null;
|
|
|
16
22
|
|
|
17
23
|
function enabled() { return api.getSetting('enabled') !== false; }
|
|
18
24
|
function safeId(id) { return String(id).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64); }
|
|
25
|
+
function pillId(pid) { return `autopilot-${pid}`; }
|
|
19
26
|
|
|
20
27
|
function outputId(text) {
|
|
21
28
|
const normalized = (text || '').trim().replace(/\s+/g, ' ');
|
|
@@ -33,6 +40,7 @@ async function ai() {
|
|
|
33
40
|
// --- KB (routing history) ---
|
|
34
41
|
|
|
35
42
|
function kbPath(pid) { return join(DATA_DIR, `${safeId(pid)}.jsonl`); }
|
|
43
|
+
function goalPath(pid) { return join(GOALS_DIR, `${safeId(pid)}.json`); }
|
|
36
44
|
|
|
37
45
|
function appendKB(pid, entry) {
|
|
38
46
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
@@ -49,6 +57,21 @@ function readKB(pid, n) {
|
|
|
49
57
|
} catch { return []; }
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
function loadGoal(pid) {
|
|
61
|
+
const p = goalPath(pid);
|
|
62
|
+
if (!existsSync(p)) return null;
|
|
63
|
+
try {
|
|
64
|
+
const goal = JSON.parse(readFileSync(p, 'utf8'));
|
|
65
|
+
return goal?.text ? goal : null;
|
|
66
|
+
} catch { return null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveGoal(pid, goal) {
|
|
70
|
+
if (!goal?.text) return;
|
|
71
|
+
mkdirSync(GOALS_DIR, { recursive: true });
|
|
72
|
+
writeFileSync(goalPath(pid), JSON.stringify(goal, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
// --- Token usage ---
|
|
53
76
|
|
|
54
77
|
function usagePath() { return join(DATA_DIR, 'usage.json'); }
|
|
@@ -82,6 +105,120 @@ function latestAgentOutput(id) {
|
|
|
82
105
|
return last?.text?.trim().slice(0, 8000) || null;
|
|
83
106
|
}
|
|
84
107
|
|
|
108
|
+
function usageTotals(usage) {
|
|
109
|
+
const input = usage?.input || 0;
|
|
110
|
+
const output = usage?.output || 0;
|
|
111
|
+
return { input, output, total: input + output };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatTokenLog(label, usage, source) {
|
|
115
|
+
const t = usageTotals(usage);
|
|
116
|
+
return `${label} tokens (${source}): in ${t.input}, out ${t.output}, total ${t.total}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function firstUserMessages(id, n) {
|
|
120
|
+
return api.getTranscript(id, FIRST_TURN_SCAN, 'start')
|
|
121
|
+
.filter(t => t.role === 'user')
|
|
122
|
+
.slice(0, n)
|
|
123
|
+
.map(t => String(t.text || '').trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractExplicitGoal(text) {
|
|
128
|
+
const lines = String(text || '').split('\n');
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
const trimmed = lines[i].trim();
|
|
131
|
+
if (!trimmed.toUpperCase().startsWith(GOAL_PREFIX)) continue;
|
|
132
|
+
const first = trimmed.slice(GOAL_PREFIX.length).trim();
|
|
133
|
+
const rest = lines.slice(i + 1).join('\n').trim();
|
|
134
|
+
return [first, rest].filter(Boolean).join('\n').replace(/\s+/g, ' ').trim();
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isUnclearGoal(text) {
|
|
140
|
+
const normalized = String(text || '').trim().replace(/^["'\s]+|["'\s]+$/g, '').toLowerCase();
|
|
141
|
+
return normalized === 'unclear_goal';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findExplicitGoal(proj) {
|
|
145
|
+
const found = [];
|
|
146
|
+
for (const [sid, w] of proj.workers) {
|
|
147
|
+
const turns = api.getTranscript(sid, GOAL_SEARCH_TURNS, 'start');
|
|
148
|
+
for (const turn of turns) {
|
|
149
|
+
if (turn.role !== 'user') continue;
|
|
150
|
+
const goal = extractExplicitGoal(turn.text);
|
|
151
|
+
if (!goal) continue;
|
|
152
|
+
found.push({ goal, worker: w.label, sessionId: sid });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const unique = [...new Set(found.map(x => x.goal))];
|
|
156
|
+
if (!unique.length) return { goal: null };
|
|
157
|
+
if (unique.length > 1) {
|
|
158
|
+
return {
|
|
159
|
+
error: `Found multiple explicit project goals. Keep one \`${GOAL_PREFIX}\` message in an existing worker session and restart Autopilot.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
goal: {
|
|
164
|
+
text: unique[0],
|
|
165
|
+
builtAt: new Date().toISOString(),
|
|
166
|
+
source: 'explicit-message',
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function goalSourceContext(proj) {
|
|
172
|
+
const sections = [];
|
|
173
|
+
for (const [sid, w] of proj.workers) {
|
|
174
|
+
const messages = firstUserMessages(sid, FIRST_USER_MESSAGES);
|
|
175
|
+
if (!messages.length) continue;
|
|
176
|
+
const body = messages.map((text, idx) => {
|
|
177
|
+
const clipped = text.length > MAX_GOAL_MESSAGE_CHARS ? text.slice(0, MAX_GOAL_MESSAGE_CHARS) : text;
|
|
178
|
+
return `Message ${idx + 1}:\n${clipped}`;
|
|
179
|
+
}).join('\n\n');
|
|
180
|
+
sections.push(`${w.name}: ${(w.role || 'Session')} first user messages:\n${body}`);
|
|
181
|
+
}
|
|
182
|
+
return sections.join('\n\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function candidateSessions(pid) {
|
|
186
|
+
return api.getSessions().filter(s => s.projectId === pid && s.presetId !== 'shell');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasUserMessage(id) {
|
|
190
|
+
return firstUserMessages(id, 1).length > 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sessionDisplayName(session) {
|
|
194
|
+
return String(session?.name || '').trim() || `Session ${String(session?.id || '').slice(0, 6)}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildRouteLabel(session, taken) {
|
|
198
|
+
const base = sessionDisplayName(session);
|
|
199
|
+
if (!taken.has(base.toLowerCase())) return base;
|
|
200
|
+
return `${base} [${String(session.id).slice(0, 6)}]`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function unpromptedSessionsMessage(sessions) {
|
|
204
|
+
const names = (sessions || []).map(sessionDisplayName).filter(Boolean);
|
|
205
|
+
const suffix = names.length ? ` Missing a first prompt: ${names.join(', ')}.` : '';
|
|
206
|
+
return `Autopilot only steers sessions after you have already told each one what to do. Give every session in this project an initial user prompt first, then start Autopilot again.${suffix}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseJsonObject(text) {
|
|
210
|
+
const trimmed = String(text || '').trim();
|
|
211
|
+
if (!trimmed) return null;
|
|
212
|
+
const cleaned = trimmed.replace(/^```json\s*|^```\s*|\s*```$/g, '').trim();
|
|
213
|
+
try { return JSON.parse(cleaned); } catch {}
|
|
214
|
+
const start = cleaned.indexOf('{');
|
|
215
|
+
const end = cleaned.lastIndexOf('}');
|
|
216
|
+
if (start >= 0 && end > start) {
|
|
217
|
+
try { return JSON.parse(cleaned.slice(start, end + 1)); } catch {}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
85
222
|
// --- Consumed state (persisted per-project: role → boolean) ---
|
|
86
223
|
|
|
87
224
|
function captureIdleOutput(id, pid, proj) {
|
|
@@ -96,9 +233,9 @@ function captureIdleOutput(id, pid, proj) {
|
|
|
96
233
|
const prev = proj.lastOutput.get(id);
|
|
97
234
|
const isNew = !prev || prev.outputId !== oid;
|
|
98
235
|
proj.lastOutput.set(id, { text: out, capturedAt: Date.now(), outputId: oid });
|
|
99
|
-
appendKB(pid, { from: w.
|
|
100
|
-
// Clear waitingOn when the awaited
|
|
101
|
-
if (isNew && proj.waitingOn?.toLowerCase() === w.
|
|
236
|
+
appendKB(pid, { from: w.label, msg: out.slice(0, 4000), outputId: oid });
|
|
237
|
+
// Clear waitingOn when the awaited worker delivers new output
|
|
238
|
+
if (isNew && proj.waitingOn?.toLowerCase() === w.label.toLowerCase()) {
|
|
102
239
|
proj.waitingOn = null;
|
|
103
240
|
proj.staleSince = null;
|
|
104
241
|
}
|
|
@@ -158,51 +295,187 @@ function projectFor(sid) {
|
|
|
158
295
|
for (const [pid, proj] of projects) {
|
|
159
296
|
if (proj.workers.has(sid)) return [pid, proj];
|
|
160
297
|
}
|
|
161
|
-
|
|
162
|
-
if (!projects.size) return [null, null];
|
|
163
|
-
const sess = api.getSessions().find(s => s.id === sid);
|
|
164
|
-
if (!sess?.projectId || !sess.roleName || !projects.has(sess.projectId)) return [null, null];
|
|
165
|
-
const pid = sess.projectId;
|
|
166
|
-
const proj = projects.get(pid);
|
|
167
|
-
const err = refreshWorkers(pid, proj);
|
|
168
|
-
if (err) {
|
|
169
|
-
api.sendToFrontend('notify', { projectId: pid, reason: `${err} — autopilot stopped` });
|
|
170
|
-
stop(pid);
|
|
171
|
-
return [null, null];
|
|
172
|
-
}
|
|
173
|
-
return proj.workers.has(sid) ? [pid, proj] : [null, null];
|
|
298
|
+
return [null, null];
|
|
174
299
|
}
|
|
175
300
|
|
|
176
|
-
function
|
|
301
|
+
function workerByLabel(proj, label) {
|
|
177
302
|
for (const [sid, w] of proj.workers) {
|
|
178
|
-
if (w.
|
|
303
|
+
if (w.label.toLowerCase() === String(label || '').toLowerCase()) return sid;
|
|
179
304
|
}
|
|
180
305
|
return null;
|
|
181
306
|
}
|
|
182
307
|
|
|
183
308
|
function isAutopilotWorkerSession(s) {
|
|
184
|
-
return s.projectId && s.
|
|
309
|
+
return s.projectId && s.presetId !== 'shell';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function inferWorkerRole(pid, session, label, goal, model, provider, apiKey) {
|
|
313
|
+
const messages = firstUserMessages(session.id, FIRST_USER_MESSAGES);
|
|
314
|
+
if (!messages.length) {
|
|
315
|
+
return {
|
|
316
|
+
error: unpromptedSessionsMessage([session]),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const context = messages.map((text, idx) => {
|
|
321
|
+
const clipped = text.length > MAX_GOAL_MESSAGE_CHARS ? text.slice(0, MAX_GOAL_MESSAGE_CHARS) : text;
|
|
322
|
+
return `Message ${idx + 1}:\n${clipped}`;
|
|
323
|
+
}).join('\n\n');
|
|
324
|
+
|
|
325
|
+
const prompt = [
|
|
326
|
+
'Hi,',
|
|
327
|
+
'Your task is to infer a role name and a concise summary of that role\'s responsibilities based on what the user has said so far to the agent. This is a crucial step for Autopilot to understand what this agent session is about and route work correctly.',
|
|
328
|
+
'',
|
|
329
|
+
'This is the project goal:',
|
|
330
|
+
'<project_goal>',
|
|
331
|
+
goalText(goal) || 'No project goal available.',
|
|
332
|
+
'</project_goal>',
|
|
333
|
+
'',
|
|
334
|
+
'Read the project goal and the user\'s messages carefully and understand the context of the project and the agent\'s role in it.',
|
|
335
|
+
'This role will be used by Autopilot to understand what this session is for and route work correctly.',
|
|
336
|
+
'',
|
|
337
|
+
'ROLE SCOPE RULES',
|
|
338
|
+
'- Do not write a plan. Do not explain your reasoning.',
|
|
339
|
+
'- Infer what this agent session is mainly responsible for in the project, its main focus, responsibilities, and what it should do or not do.',
|
|
340
|
+
'- The project can be anything: software, research, writing, design, operations, brainstorming, etc.',
|
|
341
|
+
'- If the role is unclear from the user\'s messages, return {"role":"unclear_role","summary":"unclear_role"} only.',
|
|
342
|
+
'',
|
|
343
|
+
'FORMAT RULES',
|
|
344
|
+
'- Return JSON only',
|
|
345
|
+
'- Use exactly this shape: {"role":"...","summary":"..."}',
|
|
346
|
+
'- "role" is short, like a job title or a one- or two-word label for the agent\'s main focus. e.g. "backend developer", "UX researcher", "marketing strategist", "data analyst", "project manager", "content writer", etc.',
|
|
347
|
+
'- "summary" must be a short paragraph, usually 2-5 sentences',
|
|
348
|
+
'- "summary" must explain what the agent is responsible for, what it should do, and what it should not do in this project, based on the user\'s messages and the project goal.',
|
|
349
|
+
'',
|
|
350
|
+
'EXAMPLES',
|
|
351
|
+
'Example 1 input:',
|
|
352
|
+
'You are the docs writer for this project.',
|
|
353
|
+
'Your job is to write the documentation the project needs, such as the README, setup guides, usage docs, and other project documents.',
|
|
354
|
+
'First, review the codebase, structure, and existing docs so your writing matches the real project. Do not invent behavior or features.',
|
|
355
|
+
'Write clear, accurate, concise, well-structured docs with a professional and consistent tone and style.',
|
|
356
|
+
'You do not write code unless documentation examples are needed.',
|
|
357
|
+
'',
|
|
358
|
+
'Example 1 output:',
|
|
359
|
+
'{"role":"Documentation writer","summary":"This agent is the Documentation Writer. Its role is to create the project documentation needed to help users and contributors understand, use, and work with the project clearly and correctly. It should review the codebase, structure, and existing docs before writing so the documentation matches the real project state, and it should produce clear, accurate, concise, and well-structured documents in the requested tone and style. It should not invent features or behavior, and it should not write code except when documentation examples are necessary."}',
|
|
360
|
+
'',
|
|
361
|
+
'Example 2 input:',
|
|
362
|
+
'You are the Product Manager for this project.',
|
|
363
|
+
'Your job is to understand the product deeply: what it does, why it exists, who it serves, and what the best user experience should be.',
|
|
364
|
+
'You focus on product quality above all else. Technical limitations, implementation preferences, and engineering convenience are secondary.',
|
|
365
|
+
'You never write code.',
|
|
366
|
+
'First, review the existing project if there is one. Read the codebase, documentation, and README to understand what the product is, why it was built, and what good execution should look like.',
|
|
367
|
+
'',
|
|
368
|
+
'Example 2 output:',
|
|
369
|
+
'{"role":"Product manager","summary":"This agent is the Product Manager. Its role is to understand the product end to end, define what best-in-class user experience looks like, and guide the team toward polished, high-quality outcomes with strong UI/UX standards. It should review the existing project to understand the product, its purpose, and its users, and it should evaluate decisions through the lens of user value, clarity, trust, and quality. It should not write code or optimize for engineering convenience over product quality."}',
|
|
370
|
+
'',
|
|
371
|
+
'SESSION',
|
|
372
|
+
`Label: ${label}`,
|
|
373
|
+
`Name: ${sessionDisplayName(session)}`,
|
|
374
|
+
'',
|
|
375
|
+
'SOURCE CONTEXT',
|
|
376
|
+
context,
|
|
377
|
+
].join('\n');
|
|
378
|
+
|
|
379
|
+
const res = await (await ai()).complete(model, {
|
|
380
|
+
systemPrompt: 'You infer concise neutral worker roles for workflow routing. Output JSON only.',
|
|
381
|
+
messages: [{ role: 'user', content: prompt, timestamp: Date.now() }],
|
|
382
|
+
}, { apiKey, reasoning: 'minimal' });
|
|
383
|
+
|
|
384
|
+
addTokens(pid, res.usage);
|
|
385
|
+
const raw = res.content?.find(b => b.type === 'text')?.text?.trim() || '';
|
|
386
|
+
const parsed = parseJsonObject(raw);
|
|
387
|
+
const role = String(parsed?.role || '').replace(/\s+/g, ' ').trim();
|
|
388
|
+
const summary = String(parsed?.summary || '').replace(/\s+/g, ' ').trim();
|
|
389
|
+
const unclear = role.toLowerCase() === 'unclear_role' || summary.toLowerCase() === 'unclear_role';
|
|
390
|
+
if (!parsed || unclear || !role || !summary) {
|
|
391
|
+
return {
|
|
392
|
+
unclear: true,
|
|
393
|
+
usage: res.usage,
|
|
394
|
+
notifyReason: `Autopilot could not infer a clear role for session "${label}". Add one or more user messages that make its mission clear, then start Autopilot again.`,
|
|
395
|
+
raw,
|
|
396
|
+
context,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
role,
|
|
402
|
+
summary,
|
|
403
|
+
usage: res.usage,
|
|
404
|
+
raw,
|
|
405
|
+
context,
|
|
406
|
+
};
|
|
185
407
|
}
|
|
186
408
|
|
|
187
|
-
function
|
|
409
|
+
function bootstrapWorkers(pid) {
|
|
410
|
+
const sessions = candidateSessions(pid);
|
|
411
|
+
if (!sessions.length) return { error: 'No project sessions found for Autopilot' };
|
|
412
|
+
|
|
413
|
+
const empty = sessions.filter(s => !hasUserMessage(s.id));
|
|
414
|
+
if (empty.length) {
|
|
415
|
+
return {
|
|
416
|
+
error: unpromptedSessionsMessage(empty),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
188
420
|
const workers = new Map();
|
|
189
421
|
const status = new Map();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
422
|
+
const takenLabels = new Set();
|
|
423
|
+
|
|
424
|
+
for (const session of sessions) {
|
|
425
|
+
const label = buildRouteLabel(session, takenLabels);
|
|
426
|
+
takenLabels.add(label.toLowerCase());
|
|
427
|
+
workers.set(session.id, {
|
|
428
|
+
name: sessionDisplayName(session),
|
|
429
|
+
label,
|
|
430
|
+
role: null,
|
|
431
|
+
summary: null,
|
|
432
|
+
presetId: session.presetId,
|
|
433
|
+
});
|
|
434
|
+
status.set(session.id, session.working === true);
|
|
196
435
|
}
|
|
197
436
|
return { workers, status };
|
|
198
437
|
}
|
|
199
438
|
|
|
439
|
+
async function inferRolesForWorkers(pid, proj, model, provider, apiKey) {
|
|
440
|
+
const roleUsage = { input: 0, output: 0 };
|
|
441
|
+
for (const [sid, worker] of proj.workers) {
|
|
442
|
+
const session = api.getSessions().find(s => s.id === sid);
|
|
443
|
+
if (!session) continue;
|
|
444
|
+
const inferred = await inferWorkerRole(pid, session, worker.label, proj.goal, model, provider, apiKey);
|
|
445
|
+
roleUsage.input += inferred.usage?.input || 0;
|
|
446
|
+
roleUsage.output += inferred.usage?.output || 0;
|
|
447
|
+
if (inferred.error) return { error: inferred.error, usage: roleUsage };
|
|
448
|
+
if (inferred.unclear) return { unclear: true, notifyReason: inferred.notifyReason, usage: roleUsage };
|
|
449
|
+
worker.role = inferred.role;
|
|
450
|
+
worker.summary = inferred.summary;
|
|
451
|
+
appendKB(pid, { type: 'worker-role', worker: worker.label, role: inferred.role, summary: inferred.summary });
|
|
452
|
+
api.appendPillLog(pillId(pid), `Role: ${worker.label} → ${inferred.role}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
debugLog(pid, 'roles', [
|
|
456
|
+
`# Worker Roles — ${new Date().toISOString()}`,
|
|
457
|
+
`Provider: ${provider}`,
|
|
458
|
+
`Project goal: ${goalText(proj.goal) || '(none)'}`,
|
|
459
|
+
`Tokens: in=${roleUsage.input} out=${roleUsage.output} total=${roleUsage.input + roleUsage.output}`,
|
|
460
|
+
'',
|
|
461
|
+
...[...proj.workers.values()].map(w => `- ${w.label}: ${w.role} — ${w.summary}`),
|
|
462
|
+
].join('\n'));
|
|
463
|
+
|
|
464
|
+
return { usage: roleUsage };
|
|
465
|
+
}
|
|
466
|
+
|
|
200
467
|
// Returns null on success, or an error string (caller must stop autopilot).
|
|
201
|
-
function refreshWorkers(pid, proj) {
|
|
468
|
+
async function refreshWorkers(pid, proj, model, provider, apiKey) {
|
|
469
|
+
const liveSessions = candidateSessions(pid);
|
|
202
470
|
const live = new Map();
|
|
203
471
|
const liveStatus = new Map();
|
|
204
|
-
|
|
205
|
-
|
|
472
|
+
const empty = liveSessions.filter(s => !hasUserMessage(s.id));
|
|
473
|
+
if (empty.length) {
|
|
474
|
+
return unpromptedSessionsMessage(empty);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (const s of liveSessions) {
|
|
478
|
+
live.set(s.id, s);
|
|
206
479
|
liveStatus.set(s.id, s.working === true);
|
|
207
480
|
}
|
|
208
481
|
// Remove dead sessions
|
|
@@ -215,21 +488,38 @@ function refreshWorkers(pid, proj) {
|
|
|
215
488
|
}
|
|
216
489
|
}
|
|
217
490
|
// Add new sessions
|
|
218
|
-
|
|
491
|
+
const takenLabels = new Set([...proj.workers.values()].map(w => w.label.toLowerCase()));
|
|
492
|
+
for (const [sid, session] of live) {
|
|
219
493
|
if (!proj.workers.has(sid)) {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
494
|
+
const label = buildRouteLabel(session, takenLabels);
|
|
495
|
+
takenLabels.add(label.toLowerCase());
|
|
496
|
+
const inferred = await inferWorkerRole(pid, session, label, proj.goal, model, provider, apiKey);
|
|
497
|
+
if (inferred.error) return inferred.error;
|
|
498
|
+
if (inferred.unclear) return inferred.notifyReason;
|
|
499
|
+
proj.workers.set(sid, {
|
|
500
|
+
name: sessionDisplayName(session),
|
|
501
|
+
label,
|
|
502
|
+
role: inferred.role,
|
|
503
|
+
summary: inferred.summary,
|
|
504
|
+
presetId: session.presetId,
|
|
505
|
+
});
|
|
225
506
|
proj.status.set(sid, liveStatus.get(sid));
|
|
507
|
+
appendKB(pid, { type: 'worker-role', worker: label, role: inferred.role, summary: inferred.summary, added: true });
|
|
226
508
|
api.setAutoApproveMenu(sid, true);
|
|
227
509
|
if (!liveStatus.get(sid)) {
|
|
228
510
|
const text = latestAgentOutput(sid);
|
|
229
511
|
if (text) proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
|
|
230
512
|
}
|
|
513
|
+
const pillId = `autopilot-${pid}`;
|
|
514
|
+
api.appendPillLog(pillId, `Role: ${label} → ${inferred.role}`);
|
|
231
515
|
}
|
|
232
516
|
}
|
|
517
|
+
for (const [sid, session] of live) {
|
|
518
|
+
const w = proj.workers.get(sid);
|
|
519
|
+
if (!w) continue;
|
|
520
|
+
w.name = sessionDisplayName(session);
|
|
521
|
+
proj.status.set(sid, liveStatus.get(sid));
|
|
522
|
+
}
|
|
233
523
|
return null;
|
|
234
524
|
}
|
|
235
525
|
|
|
@@ -259,6 +549,72 @@ function filterModels(all) {
|
|
|
259
549
|
.map(m => ({ value: m.id, label: `${m.name.replace(/\s*\(latest\)$/, '')} ($${m.cost.input}/M)` }));
|
|
260
550
|
}
|
|
261
551
|
|
|
552
|
+
function goalText(goal) {
|
|
553
|
+
return String(goal?.text || '').trim();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function buildGoal(pid, proj, model, provider, apiKey) {
|
|
557
|
+
const sourceContext = goalSourceContext(proj);
|
|
558
|
+
if (!sourceContext) return { error: 'No early user messages found to build project goal' };
|
|
559
|
+
|
|
560
|
+
const prompt = [
|
|
561
|
+
'Hi, You are writing a concise project goal based on early user-side messages.',
|
|
562
|
+
'This goal will be used to guide the project and keep it focused on the main objectives.',
|
|
563
|
+
'',
|
|
564
|
+
'PROJECT GOAL SCOPE RULES',
|
|
565
|
+
'- Use only the early user-side messages below.',
|
|
566
|
+
'- Do not write a plan. Do not write steps. Do not explain your reasoning.',
|
|
567
|
+
'- Write one short paragraph that captures the project mission/task/goal.',
|
|
568
|
+
'- The project can be anything: a software project, a research project, a writing project, etc. Do not assume it\'s coding work.',
|
|
569
|
+
'- If the goal is unclear, return "unclear_goal" only.',
|
|
570
|
+
'',
|
|
571
|
+
'FORMAT RULES',
|
|
572
|
+
'- One paragraph only',
|
|
573
|
+
'- Plain text only',
|
|
574
|
+
'- Keep it concise and easy to scan later',
|
|
575
|
+
'- Generic wording: do not assume this is coding work',
|
|
576
|
+
'',
|
|
577
|
+
'SOURCE CONTEXT',
|
|
578
|
+
sourceContext,
|
|
579
|
+
].join('\n');
|
|
580
|
+
|
|
581
|
+
const res = await (await ai()).complete(model, {
|
|
582
|
+
systemPrompt: 'You write concise, neutral project-goal summaries for workflow routing.',
|
|
583
|
+
messages: [{ role: 'user', content: prompt, timestamp: Date.now() }],
|
|
584
|
+
}, { apiKey, reasoning: 'minimal' });
|
|
585
|
+
|
|
586
|
+
addTokens(pid, res.usage);
|
|
587
|
+
const text = res.content?.find(b => b.type === 'text')?.text?.trim() || '';
|
|
588
|
+
if (!text) return { error: 'Model returned an empty project goal' };
|
|
589
|
+
if (isUnclearGoal(text)) {
|
|
590
|
+
return {
|
|
591
|
+
unclear: true,
|
|
592
|
+
usage: res.usage,
|
|
593
|
+
notifyReason: `Autopilot could not infer a clear project goal. Add a message starting with \`${GOAL_PREFIX}\` in any existing project worker session, then start Autopilot again.`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const goal = {
|
|
598
|
+
text: text.replace(/\s+/g, ' ').trim(),
|
|
599
|
+
builtAt: new Date().toISOString(),
|
|
600
|
+
source: 'model',
|
|
601
|
+
};
|
|
602
|
+
saveGoal(pid, goal);
|
|
603
|
+
appendKB(pid, { type: 'goal', goal: goal.text, source: goal.source, usage: usageTotals(res.usage) });
|
|
604
|
+
debugLog(pid, 'goal', [
|
|
605
|
+
`# Project Goal — ${goal.builtAt}`,
|
|
606
|
+
`Provider: ${provider}`,
|
|
607
|
+
`Tokens: in=${res.usage?.input || 0} out=${res.usage?.output || 0} total=${(res.usage?.input || 0) + (res.usage?.output || 0)}`,
|
|
608
|
+
'',
|
|
609
|
+
'## Source Context',
|
|
610
|
+
sourceContext,
|
|
611
|
+
'',
|
|
612
|
+
'## Goal',
|
|
613
|
+
goal.text,
|
|
614
|
+
].join('\n'));
|
|
615
|
+
return { goal, usage: res.usage };
|
|
616
|
+
}
|
|
617
|
+
|
|
262
618
|
async function loadModelsForProvider(provider) {
|
|
263
619
|
try {
|
|
264
620
|
const m = await ai();
|
|
@@ -289,8 +645,8 @@ function buildTools(Type) {
|
|
|
289
645
|
name: 'route',
|
|
290
646
|
description: 'Forward one agent\'s output to another idle agent. The system copies the output verbatim — you only choose who sends and who receives.',
|
|
291
647
|
parameters: Type.Object({
|
|
292
|
-
from: Type.String({ description: 'Source agent
|
|
293
|
-
to: Type.String({ description: 'Target agent
|
|
648
|
+
from: Type.String({ description: 'Source agent label (prefer [LATEST] agent)' }),
|
|
649
|
+
to: Type.String({ description: 'Target agent label (must be IDLE)' }),
|
|
294
650
|
}),
|
|
295
651
|
},
|
|
296
652
|
{
|
|
@@ -312,13 +668,10 @@ function buildPrompt(proj, pid) {
|
|
|
312
668
|
const p = pList.find(x => x.id === pid);
|
|
313
669
|
const projectName = p ? `${p.name}${p.path ? ` (${p.path})` : ''}` : pid;
|
|
314
670
|
|
|
315
|
-
const roles = api.getRoles();
|
|
316
671
|
const agentProfiles = [];
|
|
317
672
|
for (const [sid, w] of proj.workers) {
|
|
318
673
|
const busy = proj.status.get(sid);
|
|
319
|
-
|
|
320
|
-
const desc = role?.instructions || '(no role description)';
|
|
321
|
-
agentProfiles.push(`${w.role} [${busy ? 'WORKING' : 'IDLE'}]\n Role: ${desc}`);
|
|
674
|
+
agentProfiles.push(`${w.label} [${busy ? 'WORKING' : 'IDLE'}]\n Inferred role: ${w.role}\n Summary: ${w.summary}`);
|
|
322
675
|
}
|
|
323
676
|
|
|
324
677
|
let prompt = PROMPT_TEMPLATE
|
|
@@ -337,7 +690,7 @@ function buildStateContext(proj, pid) {
|
|
|
337
690
|
return `${Math.round(sec / 60)}m ago`;
|
|
338
691
|
};
|
|
339
692
|
|
|
340
|
-
const lines = ['CURRENT STATE'];
|
|
693
|
+
const lines = ['PROJECT GOAL', ` ${goalText(proj.goal) || 'Goal not set'}`, '', 'CURRENT STATE'];
|
|
341
694
|
|
|
342
695
|
// Per-worker state
|
|
343
696
|
const lastActionAt = proj.lastAction?.at || 0;
|
|
@@ -347,7 +700,7 @@ function buildStateContext(proj, pid) {
|
|
|
347
700
|
const capturedAt = stored?.capturedAt || 0;
|
|
348
701
|
const isNew = capturedAt > lastActionAt;
|
|
349
702
|
const status = proj.status.get(sid) ? 'WORKING' : 'IDLE';
|
|
350
|
-
let line = ` ${w.
|
|
703
|
+
let line = ` ${w.label}: ${status} | role: ${w.role}`;
|
|
351
704
|
if (oid) line += ` | output #${oid} captured ${fmtAgo(capturedAt)}${isNew ? ' (NEW)' : ''}`;
|
|
352
705
|
else line += ' | no output';
|
|
353
706
|
lines.push(line);
|
|
@@ -400,34 +753,23 @@ function buildStateContext(proj, pid) {
|
|
|
400
753
|
|
|
401
754
|
async function consult(pid, proj) {
|
|
402
755
|
if (proj.pending || proj.paused || !projects.has(pid)) return;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
api.sendToFrontend('notify', { projectId: pid, reason: `${refreshErr} — autopilot stopped` });
|
|
407
|
-
stop(pid);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
// Re-check all-idle after refresh — new workers may be busy
|
|
411
|
-
if (![...proj.workers.keys()].every(sid => !proj.status.get(sid))) return;
|
|
412
|
-
|
|
756
|
+
const runPillId = pillId(pid);
|
|
757
|
+
const provider = api.getSetting('provider') || 'anthropic';
|
|
758
|
+
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
413
759
|
let m;
|
|
414
760
|
try { m = await ai(); } catch (e) {
|
|
415
761
|
api.sendToFrontend('notify', { projectId: pid, reason: `Failed to load AI library: ${e.message}` });
|
|
416
762
|
stop(pid);
|
|
417
763
|
return;
|
|
418
764
|
}
|
|
419
|
-
|
|
420
|
-
const provider = api.getSetting('provider') || 'anthropic';
|
|
421
|
-
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
422
765
|
const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
|
|
423
|
-
|
|
424
766
|
if (!apiKey) {
|
|
425
767
|
proj.paused = true;
|
|
426
768
|
proj.pauseReason = 'config';
|
|
427
769
|
api.sendToFrontend('error', { msg: `No API key for ${provider}. Set in Autopilot settings or via env var.` });
|
|
428
770
|
api.sendToFrontend('paused', { projectId: pid, question: 'API key missing — configure in plugin settings' });
|
|
429
|
-
api.updateSessionPill(
|
|
430
|
-
api.appendPillLog(
|
|
771
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Paused — no API key' });
|
|
772
|
+
api.appendPillLog(runPillId, 'Paused: API key missing');
|
|
431
773
|
return;
|
|
432
774
|
}
|
|
433
775
|
|
|
@@ -438,6 +780,15 @@ async function consult(pid, proj) {
|
|
|
438
780
|
return;
|
|
439
781
|
}
|
|
440
782
|
|
|
783
|
+
const refreshErr = await refreshWorkers(pid, proj, model, provider, apiKey);
|
|
784
|
+
if (refreshErr) {
|
|
785
|
+
api.sendToFrontend('notify', { projectId: pid, reason: `${refreshErr} — autopilot stopped` });
|
|
786
|
+
stop(pid);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
// Re-check all-idle after refresh — new workers may be busy
|
|
790
|
+
if (![...proj.workers.keys()].every(sid => !proj.status.get(sid))) return;
|
|
791
|
+
|
|
441
792
|
// Build structured state context
|
|
442
793
|
const stateContext = buildStateContext(proj, pid);
|
|
443
794
|
|
|
@@ -453,7 +804,7 @@ async function consult(pid, proj) {
|
|
|
453
804
|
text = latestAgentOutput(sid);
|
|
454
805
|
if (text) { capturedAt = capturedAt || Date.now(); oid = outputId(text); }
|
|
455
806
|
}
|
|
456
|
-
entries.push({ sid, role: w.role, text, capturedAt, outputId: oid });
|
|
807
|
+
entries.push({ sid, label: w.label, role: w.role, text, capturedAt, outputId: oid });
|
|
457
808
|
}
|
|
458
809
|
entries.sort((a, b) => a.capturedAt - b.capturedAt);
|
|
459
810
|
|
|
@@ -461,7 +812,7 @@ async function consult(pid, proj) {
|
|
|
461
812
|
const isNew = e.capturedAt > lastActionAt;
|
|
462
813
|
const tag = isNew ? ' — NEW' : '';
|
|
463
814
|
const idTag = e.outputId ? ` (#${e.outputId})` : '';
|
|
464
|
-
return `[${e.
|
|
815
|
+
return `[${e.label}${tag}${idTag} | ${e.role}]:\n${e.text ? e.text.slice(0, 2000) : '(no output captured)'}`;
|
|
465
816
|
});
|
|
466
817
|
|
|
467
818
|
const sections = [stateContext, ''];
|
|
@@ -494,8 +845,8 @@ async function consult(pid, proj) {
|
|
|
494
845
|
ctx.messages[0].content,
|
|
495
846
|
].join('\n'));
|
|
496
847
|
|
|
497
|
-
api.updateSessionPill(
|
|
498
|
-
api.appendPillLog(
|
|
848
|
+
api.updateSessionPill(runPillId, { working: true, statusText: 'Consulting router...' });
|
|
849
|
+
api.appendPillLog(runPillId, `Consulting ${modelId}`);
|
|
499
850
|
// api.log(`[consult] model=${modelId} workers=${[...proj.workers.values()].map(w => w.role).join(',')}`);
|
|
500
851
|
// api.log(`[consult] hasOutput=${[...proj.workers].filter(([sid]) => proj.lastOutput.has(sid)).map(([,w]) => w.role).join(',') || 'none'}`);
|
|
501
852
|
|
|
@@ -530,7 +881,7 @@ async function consult(pid, proj) {
|
|
|
530
881
|
continue;
|
|
531
882
|
}
|
|
532
883
|
|
|
533
|
-
const error = executeAction(pid, proj, tc.name, tc.arguments,
|
|
884
|
+
const error = executeAction(pid, proj, tc.name, tc.arguments, runPillId);
|
|
534
885
|
if (error === null) { proj.pending = false; return; }
|
|
535
886
|
|
|
536
887
|
// api.log(`[consult] action error — hinting: ${error}`);
|
|
@@ -559,12 +910,13 @@ function triggerConsult(pid, proj) {
|
|
|
559
910
|
// --- Action execution (returns error string or null on success) ---
|
|
560
911
|
|
|
561
912
|
function executeAction(pid, proj, action, args, pillId) {
|
|
562
|
-
const
|
|
913
|
+
const labels = [...proj.workers.values()].map(w => w.label);
|
|
563
914
|
switch (action) {
|
|
564
915
|
case 'route': {
|
|
565
|
-
const src =
|
|
566
|
-
const dst =
|
|
567
|
-
if (!
|
|
916
|
+
const src = workerByLabel(proj, args.from);
|
|
917
|
+
const dst = workerByLabel(proj, args.to);
|
|
918
|
+
if (!src) return `No agent with label "${args.from}". Available agents: ${labels.join(', ')}`;
|
|
919
|
+
if (!dst) return `No agent with label "${args.to}". Available agents: ${labels.join(', ')}`;
|
|
568
920
|
if (src === dst) return 'Cannot route agent to itself';
|
|
569
921
|
if (proj.status.get(dst)) return `"${args.to}" is currently working — pick an idle agent`;
|
|
570
922
|
const stored = src ? proj.lastOutput.get(src) : null;
|
|
@@ -585,11 +937,12 @@ function executeAction(pid, proj, action, args, pillId) {
|
|
|
585
937
|
}
|
|
586
938
|
}
|
|
587
939
|
|
|
588
|
-
const
|
|
940
|
+
const dstWorker = proj.workers.get(dst);
|
|
589
941
|
const header = [
|
|
590
942
|
`[Autopilot route${oid ? ` | output #${oid}` : ''}]`,
|
|
591
|
-
`[Team: ${
|
|
592
|
-
`[
|
|
943
|
+
`[Team: ${labels.join(', ')}]`,
|
|
944
|
+
`[Target session: ${dstWorker?.label || args.to}]`,
|
|
945
|
+
`[Target inferred role: ${dstWorker?.role || 'unknown'}]`,
|
|
593
946
|
`[From: ${args.from}]`,
|
|
594
947
|
'[Do not spawn internal agents.]',
|
|
595
948
|
].join('\n');
|
|
@@ -633,26 +986,26 @@ async function start(pid) {
|
|
|
633
986
|
if (!enabled()) return { error: 'Autopilot disabled' };
|
|
634
987
|
|
|
635
988
|
const provider = api.getSetting('provider') || 'anthropic';
|
|
636
|
-
const
|
|
989
|
+
const m = await ai();
|
|
990
|
+
const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
|
|
637
991
|
if (!apiKey) return { error: 'Set the API key in Autopilot settings (Plugins panel)' };
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const roles = new Set();
|
|
643
|
-
for (const [, w] of workers) {
|
|
644
|
-
const key = w.role.toLowerCase();
|
|
645
|
-
if (roles.has(key)) return { error: `Duplicate role "${w.role}"` };
|
|
646
|
-
roles.add(key);
|
|
647
|
-
}
|
|
992
|
+
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
993
|
+
let model;
|
|
994
|
+
try { model = m.getModel(provider, modelId); } catch { return { error: `Model "${modelId}" not found` }; }
|
|
648
995
|
|
|
649
996
|
resetProjectState(pid);
|
|
650
997
|
resetDebugLogs(pid);
|
|
651
998
|
|
|
999
|
+
const seeded = bootstrapWorkers(pid);
|
|
1000
|
+
if (seeded.error) return { error: seeded.error };
|
|
1001
|
+
const { workers, status } = seeded;
|
|
1002
|
+
if (workers.size < 1) return { error: 'No project sessions found for Autopilot' };
|
|
1003
|
+
|
|
652
1004
|
const proj = {
|
|
653
1005
|
workers,
|
|
654
1006
|
status,
|
|
655
1007
|
lastOutput: new Map(),
|
|
1008
|
+
goal: null,
|
|
656
1009
|
paused: false,
|
|
657
1010
|
pauseReason: null,
|
|
658
1011
|
pending: false,
|
|
@@ -662,6 +1015,10 @@ async function start(pid) {
|
|
|
662
1015
|
};
|
|
663
1016
|
projects.set(pid, proj);
|
|
664
1017
|
|
|
1018
|
+
const runPillId = pillId(pid);
|
|
1019
|
+
api.addSessionPill({ id: runPillId, title: 'Autopilot', projectId: pid });
|
|
1020
|
+
api.appendPillLog(runPillId, `Started with ${workers.size} workers: ${[...workers.values()].map(w => w.label).join(', ')}`);
|
|
1021
|
+
|
|
665
1022
|
// Flag all workers for core menu auto-approve
|
|
666
1023
|
for (const [sid] of workers) api.setAutoApproveMenu(sid, true);
|
|
667
1024
|
|
|
@@ -674,9 +1031,75 @@ async function start(pid) {
|
|
|
674
1031
|
|
|
675
1032
|
// api.log(`Started: ${pid}, ${workers.size} workers`);
|
|
676
1033
|
api.sendToFrontend('started', { projectId: pid });
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1034
|
+
|
|
1035
|
+
const explicit = findExplicitGoal(proj);
|
|
1036
|
+
if (explicit.error) {
|
|
1037
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Goal conflict' });
|
|
1038
|
+
api.appendPillLog(runPillId, `Notify: ${explicit.error}`);
|
|
1039
|
+
api.sendToFrontend('notify', { projectId: pid, reason: explicit.error });
|
|
1040
|
+
stop(pid, true, 'Stopped');
|
|
1041
|
+
return { error: explicit.error };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (explicit.goal) {
|
|
1045
|
+
proj.goal = explicit.goal;
|
|
1046
|
+
saveGoal(pid, explicit.goal);
|
|
1047
|
+
appendKB(pid, { type: 'goal', goal: explicit.goal.text, source: explicit.goal.source });
|
|
1048
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Project goal ready' });
|
|
1049
|
+
api.appendPillLog(runPillId, `Goal: ${explicit.goal.text}`);
|
|
1050
|
+
api.appendPillLog(runPillId, formatTokenLog('Goal', null, 'explicit message'));
|
|
1051
|
+
} else {
|
|
1052
|
+
const savedGoal = loadGoal(pid);
|
|
1053
|
+
if (savedGoal) {
|
|
1054
|
+
proj.goal = savedGoal;
|
|
1055
|
+
appendKB(pid, { type: 'goal', goal: savedGoal.text, source: savedGoal.source || 'saved', loaded: true });
|
|
1056
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Project goal ready' });
|
|
1057
|
+
api.appendPillLog(runPillId, `Goal: ${savedGoal.text}`);
|
|
1058
|
+
} else {
|
|
1059
|
+
proj.pending = true;
|
|
1060
|
+
api.updateSessionPill(runPillId, { working: true, statusText: 'Building project goal...' });
|
|
1061
|
+
api.appendPillLog(runPillId, 'Building project goal');
|
|
1062
|
+
const built = await buildGoal(pid, proj, model, provider, apiKey);
|
|
1063
|
+
proj.pending = false;
|
|
1064
|
+
if (built.error) {
|
|
1065
|
+
api.sendToFrontend('notify', { projectId: pid, reason: `${built.error} — autopilot stopped` });
|
|
1066
|
+
stop(pid, true, 'Stopped');
|
|
1067
|
+
return { error: built.error };
|
|
1068
|
+
}
|
|
1069
|
+
if (built.unclear) {
|
|
1070
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Goal required' });
|
|
1071
|
+
api.appendPillLog(runPillId, 'Goal: unclear_goal');
|
|
1072
|
+
api.appendPillLog(runPillId, formatTokenLog('Goal', built.usage, 'goal build'));
|
|
1073
|
+
api.appendPillLog(runPillId, `Notify: ${built.notifyReason}`);
|
|
1074
|
+
api.sendToFrontend('notify', { projectId: pid, reason: built.notifyReason });
|
|
1075
|
+
stop(pid, true, 'Goal required');
|
|
1076
|
+
return { error: 'unclear_goal' };
|
|
1077
|
+
}
|
|
1078
|
+
proj.goal = built.goal;
|
|
1079
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Project goal ready' });
|
|
1080
|
+
api.appendPillLog(runPillId, `Goal: ${built.goal.text}`);
|
|
1081
|
+
api.appendPillLog(runPillId, formatTokenLog('Goal', built.usage, 'goal build'));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
api.updateSessionPill(runPillId, { working: true, statusText: 'Inferring agent roles...' });
|
|
1086
|
+
api.appendPillLog(runPillId, 'Inferring agent roles');
|
|
1087
|
+
const roleResult = await inferRolesForWorkers(pid, proj, model, provider, apiKey);
|
|
1088
|
+
if (roleResult.error) {
|
|
1089
|
+
api.sendToFrontend('notify', { projectId: pid, reason: `${roleResult.error} — autopilot stopped` });
|
|
1090
|
+
stop(pid, true, 'Stopped');
|
|
1091
|
+
return { error: roleResult.error };
|
|
1092
|
+
}
|
|
1093
|
+
if (roleResult.unclear) {
|
|
1094
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Role required' });
|
|
1095
|
+
api.appendPillLog(runPillId, `Notify: ${roleResult.notifyReason}`);
|
|
1096
|
+
api.appendPillLog(runPillId, formatTokenLog('Role', roleResult.usage, 'role inference'));
|
|
1097
|
+
api.sendToFrontend('notify', { projectId: pid, reason: roleResult.notifyReason });
|
|
1098
|
+
stop(pid, true, 'Role required');
|
|
1099
|
+
return { error: 'unclear_role' };
|
|
1100
|
+
}
|
|
1101
|
+
api.appendPillLog(runPillId, formatTokenLog('Role', roleResult.usage, 'role inference'));
|
|
1102
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Ready' });
|
|
680
1103
|
|
|
681
1104
|
// Only consult if all workers are already idle
|
|
682
1105
|
const allIdle = [...workers.keys()].every(sid => !status.get(sid));
|
|
@@ -685,7 +1108,7 @@ async function start(pid) {
|
|
|
685
1108
|
return { ok: true };
|
|
686
1109
|
}
|
|
687
1110
|
|
|
688
|
-
function stop(pid, keepPill) {
|
|
1111
|
+
function stop(pid, keepPill, finalLog) {
|
|
689
1112
|
const proj = projects.get(pid);
|
|
690
1113
|
if (!proj) return;
|
|
691
1114
|
for (const [sid] of proj.workers) clearIdleCaptureTimer(sid);
|
|
@@ -695,7 +1118,7 @@ function stop(pid, keepPill) {
|
|
|
695
1118
|
api.sendToFrontend('stopped', { projectId: pid });
|
|
696
1119
|
const pillId = `autopilot-${pid}`;
|
|
697
1120
|
if (keepPill) {
|
|
698
|
-
api.appendPillLog(pillId, 'Completed');
|
|
1121
|
+
api.appendPillLog(pillId, finalLog || 'Completed');
|
|
699
1122
|
} else {
|
|
700
1123
|
api.appendPillLog(pillId, 'Stopped');
|
|
701
1124
|
api.removeSessionPill(pillId);
|
|
@@ -742,7 +1165,7 @@ module.exports.init = function (pluginApi) {
|
|
|
742
1165
|
const [pid, proj] = projectFor(id);
|
|
743
1166
|
if (!pid) return;
|
|
744
1167
|
const w = proj.workers.get(id);
|
|
745
|
-
const role = w?.
|
|
1168
|
+
const role = w?.label || id.slice(0, 8);
|
|
746
1169
|
|
|
747
1170
|
if (working) {
|
|
748
1171
|
menuPending.delete(id);
|