clideck 1.30.1 → 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/agent-presets.json +2 -2
- package/config.js +6 -3
- 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 +537 -91
- 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 +29 -16
- package/public/js/creator.js +45 -87
- package/public/js/nav.js +2 -2
- package/public/js/terminals.js +77 -17
- package/public/tailwind.css +1 -1
- package/sessions.js +8 -2
- package/skills/research-experiment/SKILL.md +224 -0
- package/skills/research-experiment/agents/openai.yaml +4 -0
- package/telemetry-receiver.js +51 -15
- package/transcript.js +27 -16
|
@@ -5,16 +5,24 @@ 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
|
|
11
17
|
const tokenUsage = new Map(); // projectId → { input, output }
|
|
12
18
|
const menuPending = new Set(); // sessionIds with a menu awaiting auto-approve
|
|
19
|
+
const idleCaptureTimers = new Map(); // sessionId → timeout id for deferred idle capture
|
|
13
20
|
let api = null;
|
|
14
21
|
let piAi = null;
|
|
15
22
|
|
|
16
23
|
function enabled() { return api.getSetting('enabled') !== false; }
|
|
17
24
|
function safeId(id) { return String(id).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64); }
|
|
25
|
+
function pillId(pid) { return `autopilot-${pid}`; }
|
|
18
26
|
|
|
19
27
|
function outputId(text) {
|
|
20
28
|
const normalized = (text || '').trim().replace(/\s+/g, ' ');
|
|
@@ -32,6 +40,7 @@ async function ai() {
|
|
|
32
40
|
// --- KB (routing history) ---
|
|
33
41
|
|
|
34
42
|
function kbPath(pid) { return join(DATA_DIR, `${safeId(pid)}.jsonl`); }
|
|
43
|
+
function goalPath(pid) { return join(GOALS_DIR, `${safeId(pid)}.json`); }
|
|
35
44
|
|
|
36
45
|
function appendKB(pid, entry) {
|
|
37
46
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
@@ -48,6 +57,21 @@ function readKB(pid, n) {
|
|
|
48
57
|
} catch { return []; }
|
|
49
58
|
}
|
|
50
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
|
+
|
|
51
75
|
// --- Token usage ---
|
|
52
76
|
|
|
53
77
|
function usagePath() { return join(DATA_DIR, 'usage.json'); }
|
|
@@ -81,9 +105,124 @@ function latestAgentOutput(id) {
|
|
|
81
105
|
return last?.text?.trim().slice(0, 8000) || null;
|
|
82
106
|
}
|
|
83
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
|
+
|
|
84
222
|
// --- Consumed state (persisted per-project: role → boolean) ---
|
|
85
223
|
|
|
86
224
|
function captureIdleOutput(id, pid, proj) {
|
|
225
|
+
idleCaptureTimers.delete(id);
|
|
87
226
|
if (!projects.has(pid)) return;
|
|
88
227
|
if (proj.status.get(id)) return;
|
|
89
228
|
const w = proj.workers.get(id);
|
|
@@ -94,9 +233,9 @@ function captureIdleOutput(id, pid, proj) {
|
|
|
94
233
|
const prev = proj.lastOutput.get(id);
|
|
95
234
|
const isNew = !prev || prev.outputId !== oid;
|
|
96
235
|
proj.lastOutput.set(id, { text: out, capturedAt: Date.now(), outputId: oid });
|
|
97
|
-
appendKB(pid, { from: w.
|
|
98
|
-
// Clear waitingOn when the awaited
|
|
99
|
-
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()) {
|
|
100
239
|
proj.waitingOn = null;
|
|
101
240
|
proj.staleSince = null;
|
|
102
241
|
}
|
|
@@ -114,6 +253,19 @@ function captureIdleOutput(id, pid, proj) {
|
|
|
114
253
|
}
|
|
115
254
|
}
|
|
116
255
|
|
|
256
|
+
function clearIdleCaptureTimer(id) {
|
|
257
|
+
const timer = idleCaptureTimers.get(id);
|
|
258
|
+
if (timer) {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
idleCaptureTimers.delete(id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function scheduleIdleCapture(id, pid, proj) {
|
|
265
|
+
clearIdleCaptureTimer(id);
|
|
266
|
+
idleCaptureTimers.set(id, setTimeout(() => captureIdleOutput(id, pid, proj), 5000));
|
|
267
|
+
}
|
|
268
|
+
|
|
117
269
|
function resetProjectState(pid) {
|
|
118
270
|
try { unlinkSync(kbPath(pid)); } catch {}
|
|
119
271
|
}
|
|
@@ -143,51 +295,187 @@ function projectFor(sid) {
|
|
|
143
295
|
for (const [pid, proj] of projects) {
|
|
144
296
|
if (proj.workers.has(sid)) return [pid, proj];
|
|
145
297
|
}
|
|
146
|
-
|
|
147
|
-
if (!projects.size) return [null, null];
|
|
148
|
-
const sess = api.getSessions().find(s => s.id === sid);
|
|
149
|
-
if (!sess?.projectId || !sess.roleName || !projects.has(sess.projectId)) return [null, null];
|
|
150
|
-
const pid = sess.projectId;
|
|
151
|
-
const proj = projects.get(pid);
|
|
152
|
-
const err = refreshWorkers(pid, proj);
|
|
153
|
-
if (err) {
|
|
154
|
-
api.sendToFrontend('notify', { projectId: pid, reason: `${err} — autopilot stopped` });
|
|
155
|
-
stop(pid);
|
|
156
|
-
return [null, null];
|
|
157
|
-
}
|
|
158
|
-
return proj.workers.has(sid) ? [pid, proj] : [null, null];
|
|
298
|
+
return [null, null];
|
|
159
299
|
}
|
|
160
300
|
|
|
161
|
-
function
|
|
301
|
+
function workerByLabel(proj, label) {
|
|
162
302
|
for (const [sid, w] of proj.workers) {
|
|
163
|
-
if (w.
|
|
303
|
+
if (w.label.toLowerCase() === String(label || '').toLowerCase()) return sid;
|
|
164
304
|
}
|
|
165
305
|
return null;
|
|
166
306
|
}
|
|
167
307
|
|
|
168
308
|
function isAutopilotWorkerSession(s) {
|
|
169
|
-
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
|
+
};
|
|
170
407
|
}
|
|
171
408
|
|
|
172
|
-
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
|
+
|
|
173
420
|
const workers = new Map();
|
|
174
421
|
const status = new Map();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
435
|
}
|
|
182
436
|
return { workers, status };
|
|
183
437
|
}
|
|
184
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
|
+
|
|
185
467
|
// Returns null on success, or an error string (caller must stop autopilot).
|
|
186
|
-
function refreshWorkers(pid, proj) {
|
|
468
|
+
async function refreshWorkers(pid, proj, model, provider, apiKey) {
|
|
469
|
+
const liveSessions = candidateSessions(pid);
|
|
187
470
|
const live = new Map();
|
|
188
471
|
const liveStatus = new Map();
|
|
189
|
-
|
|
190
|
-
|
|
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);
|
|
191
479
|
liveStatus.set(s.id, s.working === true);
|
|
192
480
|
}
|
|
193
481
|
// Remove dead sessions
|
|
@@ -200,21 +488,38 @@ function refreshWorkers(pid, proj) {
|
|
|
200
488
|
}
|
|
201
489
|
}
|
|
202
490
|
// Add new sessions
|
|
203
|
-
|
|
491
|
+
const takenLabels = new Set([...proj.workers.values()].map(w => w.label.toLowerCase()));
|
|
492
|
+
for (const [sid, session] of live) {
|
|
204
493
|
if (!proj.workers.has(sid)) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
});
|
|
210
506
|
proj.status.set(sid, liveStatus.get(sid));
|
|
507
|
+
appendKB(pid, { type: 'worker-role', worker: label, role: inferred.role, summary: inferred.summary, added: true });
|
|
211
508
|
api.setAutoApproveMenu(sid, true);
|
|
212
509
|
if (!liveStatus.get(sid)) {
|
|
213
510
|
const text = latestAgentOutput(sid);
|
|
214
511
|
if (text) proj.lastOutput.set(sid, { text, capturedAt: Date.now(), outputId: outputId(text) });
|
|
215
512
|
}
|
|
513
|
+
const pillId = `autopilot-${pid}`;
|
|
514
|
+
api.appendPillLog(pillId, `Role: ${label} → ${inferred.role}`);
|
|
216
515
|
}
|
|
217
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
|
+
}
|
|
218
523
|
return null;
|
|
219
524
|
}
|
|
220
525
|
|
|
@@ -244,6 +549,72 @@ function filterModels(all) {
|
|
|
244
549
|
.map(m => ({ value: m.id, label: `${m.name.replace(/\s*\(latest\)$/, '')} ($${m.cost.input}/M)` }));
|
|
245
550
|
}
|
|
246
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
|
+
|
|
247
618
|
async function loadModelsForProvider(provider) {
|
|
248
619
|
try {
|
|
249
620
|
const m = await ai();
|
|
@@ -274,8 +645,8 @@ function buildTools(Type) {
|
|
|
274
645
|
name: 'route',
|
|
275
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.',
|
|
276
647
|
parameters: Type.Object({
|
|
277
|
-
from: Type.String({ description: 'Source agent
|
|
278
|
-
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)' }),
|
|
279
650
|
}),
|
|
280
651
|
},
|
|
281
652
|
{
|
|
@@ -297,13 +668,10 @@ function buildPrompt(proj, pid) {
|
|
|
297
668
|
const p = pList.find(x => x.id === pid);
|
|
298
669
|
const projectName = p ? `${p.name}${p.path ? ` (${p.path})` : ''}` : pid;
|
|
299
670
|
|
|
300
|
-
const roles = api.getRoles();
|
|
301
671
|
const agentProfiles = [];
|
|
302
672
|
for (const [sid, w] of proj.workers) {
|
|
303
673
|
const busy = proj.status.get(sid);
|
|
304
|
-
|
|
305
|
-
const desc = role?.instructions || '(no role description)';
|
|
306
|
-
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}`);
|
|
307
675
|
}
|
|
308
676
|
|
|
309
677
|
let prompt = PROMPT_TEMPLATE
|
|
@@ -322,7 +690,7 @@ function buildStateContext(proj, pid) {
|
|
|
322
690
|
return `${Math.round(sec / 60)}m ago`;
|
|
323
691
|
};
|
|
324
692
|
|
|
325
|
-
const lines = ['CURRENT STATE'];
|
|
693
|
+
const lines = ['PROJECT GOAL', ` ${goalText(proj.goal) || 'Goal not set'}`, '', 'CURRENT STATE'];
|
|
326
694
|
|
|
327
695
|
// Per-worker state
|
|
328
696
|
const lastActionAt = proj.lastAction?.at || 0;
|
|
@@ -332,7 +700,7 @@ function buildStateContext(proj, pid) {
|
|
|
332
700
|
const capturedAt = stored?.capturedAt || 0;
|
|
333
701
|
const isNew = capturedAt > lastActionAt;
|
|
334
702
|
const status = proj.status.get(sid) ? 'WORKING' : 'IDLE';
|
|
335
|
-
let line = ` ${w.
|
|
703
|
+
let line = ` ${w.label}: ${status} | role: ${w.role}`;
|
|
336
704
|
if (oid) line += ` | output #${oid} captured ${fmtAgo(capturedAt)}${isNew ? ' (NEW)' : ''}`;
|
|
337
705
|
else line += ' | no output';
|
|
338
706
|
lines.push(line);
|
|
@@ -385,34 +753,23 @@ function buildStateContext(proj, pid) {
|
|
|
385
753
|
|
|
386
754
|
async function consult(pid, proj) {
|
|
387
755
|
if (proj.pending || proj.paused || !projects.has(pid)) return;
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
api.sendToFrontend('notify', { projectId: pid, reason: `${refreshErr} — autopilot stopped` });
|
|
392
|
-
stop(pid);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// Re-check all-idle after refresh — new workers may be busy
|
|
396
|
-
if (![...proj.workers.keys()].every(sid => !proj.status.get(sid))) return;
|
|
397
|
-
|
|
756
|
+
const runPillId = pillId(pid);
|
|
757
|
+
const provider = api.getSetting('provider') || 'anthropic';
|
|
758
|
+
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
398
759
|
let m;
|
|
399
760
|
try { m = await ai(); } catch (e) {
|
|
400
761
|
api.sendToFrontend('notify', { projectId: pid, reason: `Failed to load AI library: ${e.message}` });
|
|
401
762
|
stop(pid);
|
|
402
763
|
return;
|
|
403
764
|
}
|
|
404
|
-
|
|
405
|
-
const provider = api.getSetting('provider') || 'anthropic';
|
|
406
|
-
const modelId = api.getSetting('model') || 'claude-opus-4-6';
|
|
407
765
|
const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
|
|
408
|
-
|
|
409
766
|
if (!apiKey) {
|
|
410
767
|
proj.paused = true;
|
|
411
768
|
proj.pauseReason = 'config';
|
|
412
769
|
api.sendToFrontend('error', { msg: `No API key for ${provider}. Set in Autopilot settings or via env var.` });
|
|
413
770
|
api.sendToFrontend('paused', { projectId: pid, question: 'API key missing — configure in plugin settings' });
|
|
414
|
-
api.updateSessionPill(
|
|
415
|
-
api.appendPillLog(
|
|
771
|
+
api.updateSessionPill(runPillId, { working: false, statusText: 'Paused — no API key' });
|
|
772
|
+
api.appendPillLog(runPillId, 'Paused: API key missing');
|
|
416
773
|
return;
|
|
417
774
|
}
|
|
418
775
|
|
|
@@ -423,6 +780,15 @@ async function consult(pid, proj) {
|
|
|
423
780
|
return;
|
|
424
781
|
}
|
|
425
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
|
+
|
|
426
792
|
// Build structured state context
|
|
427
793
|
const stateContext = buildStateContext(proj, pid);
|
|
428
794
|
|
|
@@ -438,7 +804,7 @@ async function consult(pid, proj) {
|
|
|
438
804
|
text = latestAgentOutput(sid);
|
|
439
805
|
if (text) { capturedAt = capturedAt || Date.now(); oid = outputId(text); }
|
|
440
806
|
}
|
|
441
|
-
entries.push({ sid, role: w.role, text, capturedAt, outputId: oid });
|
|
807
|
+
entries.push({ sid, label: w.label, role: w.role, text, capturedAt, outputId: oid });
|
|
442
808
|
}
|
|
443
809
|
entries.sort((a, b) => a.capturedAt - b.capturedAt);
|
|
444
810
|
|
|
@@ -446,7 +812,7 @@ async function consult(pid, proj) {
|
|
|
446
812
|
const isNew = e.capturedAt > lastActionAt;
|
|
447
813
|
const tag = isNew ? ' — NEW' : '';
|
|
448
814
|
const idTag = e.outputId ? ` (#${e.outputId})` : '';
|
|
449
|
-
return `[${e.
|
|
815
|
+
return `[${e.label}${tag}${idTag} | ${e.role}]:\n${e.text ? e.text.slice(0, 2000) : '(no output captured)'}`;
|
|
450
816
|
});
|
|
451
817
|
|
|
452
818
|
const sections = [stateContext, ''];
|
|
@@ -479,8 +845,8 @@ async function consult(pid, proj) {
|
|
|
479
845
|
ctx.messages[0].content,
|
|
480
846
|
].join('\n'));
|
|
481
847
|
|
|
482
|
-
api.updateSessionPill(
|
|
483
|
-
api.appendPillLog(
|
|
848
|
+
api.updateSessionPill(runPillId, { working: true, statusText: 'Consulting router...' });
|
|
849
|
+
api.appendPillLog(runPillId, `Consulting ${modelId}`);
|
|
484
850
|
// api.log(`[consult] model=${modelId} workers=${[...proj.workers.values()].map(w => w.role).join(',')}`);
|
|
485
851
|
// api.log(`[consult] hasOutput=${[...proj.workers].filter(([sid]) => proj.lastOutput.has(sid)).map(([,w]) => w.role).join(',') || 'none'}`);
|
|
486
852
|
|
|
@@ -515,7 +881,7 @@ async function consult(pid, proj) {
|
|
|
515
881
|
continue;
|
|
516
882
|
}
|
|
517
883
|
|
|
518
|
-
const error = executeAction(pid, proj, tc.name, tc.arguments,
|
|
884
|
+
const error = executeAction(pid, proj, tc.name, tc.arguments, runPillId);
|
|
519
885
|
if (error === null) { proj.pending = false; return; }
|
|
520
886
|
|
|
521
887
|
// api.log(`[consult] action error — hinting: ${error}`);
|
|
@@ -544,12 +910,13 @@ function triggerConsult(pid, proj) {
|
|
|
544
910
|
// --- Action execution (returns error string or null on success) ---
|
|
545
911
|
|
|
546
912
|
function executeAction(pid, proj, action, args, pillId) {
|
|
547
|
-
const
|
|
913
|
+
const labels = [...proj.workers.values()].map(w => w.label);
|
|
548
914
|
switch (action) {
|
|
549
915
|
case 'route': {
|
|
550
|
-
const src =
|
|
551
|
-
const dst =
|
|
552
|
-
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(', ')}`;
|
|
553
920
|
if (src === dst) return 'Cannot route agent to itself';
|
|
554
921
|
if (proj.status.get(dst)) return `"${args.to}" is currently working — pick an idle agent`;
|
|
555
922
|
const stored = src ? proj.lastOutput.get(src) : null;
|
|
@@ -570,11 +937,12 @@ function executeAction(pid, proj, action, args, pillId) {
|
|
|
570
937
|
}
|
|
571
938
|
}
|
|
572
939
|
|
|
573
|
-
const
|
|
940
|
+
const dstWorker = proj.workers.get(dst);
|
|
574
941
|
const header = [
|
|
575
942
|
`[Autopilot route${oid ? ` | output #${oid}` : ''}]`,
|
|
576
|
-
`[Team: ${
|
|
577
|
-
`[
|
|
943
|
+
`[Team: ${labels.join(', ')}]`,
|
|
944
|
+
`[Target session: ${dstWorker?.label || args.to}]`,
|
|
945
|
+
`[Target inferred role: ${dstWorker?.role || 'unknown'}]`,
|
|
578
946
|
`[From: ${args.from}]`,
|
|
579
947
|
'[Do not spawn internal agents.]',
|
|
580
948
|
].join('\n');
|
|
@@ -618,26 +986,26 @@ async function start(pid) {
|
|
|
618
986
|
if (!enabled()) return { error: 'Autopilot disabled' };
|
|
619
987
|
|
|
620
988
|
const provider = api.getSetting('provider') || 'anthropic';
|
|
621
|
-
const
|
|
989
|
+
const m = await ai();
|
|
990
|
+
const apiKey = api.getSetting('apiKey') || m.getEnvApiKey(provider) || '';
|
|
622
991
|
if (!apiKey) return { error: 'Set the API key in Autopilot settings (Plugins panel)' };
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const roles = new Set();
|
|
628
|
-
for (const [, w] of workers) {
|
|
629
|
-
const key = w.role.toLowerCase();
|
|
630
|
-
if (roles.has(key)) return { error: `Duplicate role "${w.role}"` };
|
|
631
|
-
roles.add(key);
|
|
632
|
-
}
|
|
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` }; }
|
|
633
995
|
|
|
634
996
|
resetProjectState(pid);
|
|
635
997
|
resetDebugLogs(pid);
|
|
636
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
|
+
|
|
637
1004
|
const proj = {
|
|
638
1005
|
workers,
|
|
639
1006
|
status,
|
|
640
1007
|
lastOutput: new Map(),
|
|
1008
|
+
goal: null,
|
|
641
1009
|
paused: false,
|
|
642
1010
|
pauseReason: null,
|
|
643
1011
|
pending: false,
|
|
@@ -647,6 +1015,10 @@ async function start(pid) {
|
|
|
647
1015
|
};
|
|
648
1016
|
projects.set(pid, proj);
|
|
649
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
|
+
|
|
650
1022
|
// Flag all workers for core menu auto-approve
|
|
651
1023
|
for (const [sid] of workers) api.setAutoApproveMenu(sid, true);
|
|
652
1024
|
|
|
@@ -659,9 +1031,75 @@ async function start(pid) {
|
|
|
659
1031
|
|
|
660
1032
|
// api.log(`Started: ${pid}, ${workers.size} workers`);
|
|
661
1033
|
api.sendToFrontend('started', { projectId: pid });
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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' });
|
|
665
1103
|
|
|
666
1104
|
// Only consult if all workers are already idle
|
|
667
1105
|
const allIdle = [...workers.keys()].every(sid => !status.get(sid));
|
|
@@ -670,16 +1108,17 @@ async function start(pid) {
|
|
|
670
1108
|
return { ok: true };
|
|
671
1109
|
}
|
|
672
1110
|
|
|
673
|
-
function stop(pid, keepPill) {
|
|
1111
|
+
function stop(pid, keepPill, finalLog) {
|
|
674
1112
|
const proj = projects.get(pid);
|
|
675
1113
|
if (!proj) return;
|
|
1114
|
+
for (const [sid] of proj.workers) clearIdleCaptureTimer(sid);
|
|
676
1115
|
for (const [sid] of proj.workers) api.setAutoApproveMenu(sid, false);
|
|
677
1116
|
projects.delete(pid);
|
|
678
1117
|
// api.log(`Stopped: ${pid}`);
|
|
679
1118
|
api.sendToFrontend('stopped', { projectId: pid });
|
|
680
1119
|
const pillId = `autopilot-${pid}`;
|
|
681
1120
|
if (keepPill) {
|
|
682
|
-
api.appendPillLog(pillId, 'Completed');
|
|
1121
|
+
api.appendPillLog(pillId, finalLog || 'Completed');
|
|
683
1122
|
} else {
|
|
684
1123
|
api.appendPillLog(pillId, 'Stopped');
|
|
685
1124
|
api.removeSessionPill(pillId);
|
|
@@ -714,7 +1153,10 @@ module.exports.init = function (pluginApi) {
|
|
|
714
1153
|
api.onMenuDetected((id, choices) => {
|
|
715
1154
|
if (!choices?.length) return;
|
|
716
1155
|
const [pid] = projectFor(id);
|
|
717
|
-
if (pid)
|
|
1156
|
+
if (pid) {
|
|
1157
|
+
menuPending.add(id);
|
|
1158
|
+
clearIdleCaptureTimer(id);
|
|
1159
|
+
}
|
|
718
1160
|
});
|
|
719
1161
|
|
|
720
1162
|
// Status change — the main routing trigger (only when ALL workers are idle)
|
|
@@ -723,11 +1165,15 @@ module.exports.init = function (pluginApi) {
|
|
|
723
1165
|
const [pid, proj] = projectFor(id);
|
|
724
1166
|
if (!pid) return;
|
|
725
1167
|
const w = proj.workers.get(id);
|
|
726
|
-
const role = w?.
|
|
1168
|
+
const role = w?.label || id.slice(0, 8);
|
|
727
1169
|
|
|
728
|
-
if (working) {
|
|
1170
|
+
if (working) {
|
|
1171
|
+
menuPending.delete(id);
|
|
1172
|
+
clearIdleCaptureTimer(id);
|
|
1173
|
+
}
|
|
729
1174
|
|
|
730
1175
|
if (!working && menuPending.has(id)) {
|
|
1176
|
+
clearIdleCaptureTimer(id);
|
|
731
1177
|
menuPending.delete(id);
|
|
732
1178
|
// api.log(`[status] ${role} → IDLE (menu pending — suppressed)`);
|
|
733
1179
|
return;
|
|
@@ -744,7 +1190,7 @@ module.exports.init = function (pluginApi) {
|
|
|
744
1190
|
if (!proj.workers.has(id)) return;
|
|
745
1191
|
|
|
746
1192
|
api.appendPillLog(pillId, `${role} → idle`);
|
|
747
|
-
|
|
1193
|
+
scheduleIdleCapture(id, pid, proj);
|
|
748
1194
|
});
|
|
749
1195
|
|
|
750
1196
|
// Frontend queries
|