@yemi33/minions 0.1.58 → 0.1.60
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/CHANGELOG.md +39 -0
- package/dashboard/js/command-center.js +1 -1
- package/dashboard/js/live-stream.js +3 -3
- package/dashboard/js/modal-qa.js +4 -4
- package/dashboard/js/refresh.js +2 -2
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-kb.js +1 -1
- package/dashboard/js/render-plans.js +3 -3
- package/dashboard/js/render-prs.js +2 -2
- package/dashboard/js/render-work-items.js +2 -2
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +46 -46
- package/engine/ado.js +2 -2
- package/engine/cli.js +16 -16
- package/engine/consolidation.js +4 -4
- package/engine/cooldown.js +117 -0
- package/engine/github.js +3 -3
- package/engine/lifecycle.js +14 -14
- package/engine/llm.js +2 -2
- package/engine/playbook.js +479 -0
- package/engine/preflight.js +2 -2
- package/engine/queries.js +13 -13
- package/engine/routing.js +163 -0
- package/engine/shared.js +6 -6
- package/engine/spawn-agent.js +3 -3
- package/engine.js +106 -733
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -123,516 +123,17 @@ const { getConfig, getControl, getDispatch, getNotes,
|
|
|
123
123
|
collectSkillFiles, getSkillIndex, getKnowledgeBaseIndex,
|
|
124
124
|
getPrs, SKILLS_DIR } = queries;
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
return safeRead(ROUTING_PATH);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ─── Routing Parser ─────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
let _routingCache = null;
|
|
133
|
-
let _routingCacheMtime = 0;
|
|
134
|
-
|
|
135
|
-
function parseRoutingTable() {
|
|
136
|
-
const content = getRouting();
|
|
137
|
-
const routes = {};
|
|
138
|
-
const lines = content.split('\n');
|
|
139
|
-
let inTable = false;
|
|
140
|
-
|
|
141
|
-
for (const line of lines) {
|
|
142
|
-
if (line.startsWith('| Work Type')) { inTable = true; continue; }
|
|
143
|
-
if (line.startsWith('|---')) continue;
|
|
144
|
-
if (!inTable || !line.startsWith('|')) {
|
|
145
|
-
if (inTable && !line.startsWith('|')) inTable = false;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
149
|
-
if (cells.length >= 3) {
|
|
150
|
-
routes[cells[0].toLowerCase()] = {
|
|
151
|
-
preferred: cells[1].toLowerCase(),
|
|
152
|
-
fallback: cells[2].toLowerCase()
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return routes;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getRoutingTableCached() {
|
|
160
|
-
let mtime = 0;
|
|
161
|
-
try { mtime = fs.statSync(ROUTING_PATH).mtimeMs; } catch {}
|
|
162
|
-
if (_routingCache && _routingCacheMtime === mtime) return _routingCache;
|
|
163
|
-
_routingCache = parseRoutingTable();
|
|
164
|
-
_routingCacheMtime = mtime;
|
|
165
|
-
return _routingCache;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function getMonthlySpend(agentId) {
|
|
169
|
-
const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
|
|
170
|
-
const daily = metrics._daily || {};
|
|
171
|
-
const now = new Date();
|
|
172
|
-
const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
173
|
-
let total = 0;
|
|
174
|
-
for (const [date, data] of Object.entries(daily)) {
|
|
175
|
-
if (date.startsWith(monthPrefix)) {
|
|
176
|
-
total += (data.perAgent?.[agentId]?.costUsd || 0);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// Fallback: if no per-agent daily data, use cumulative (less accurate for monthly)
|
|
180
|
-
if (total === 0 && metrics[agentId]?.totalCostUsd) {
|
|
181
|
-
// Can't distinguish monthly from cumulative — treat as monthly estimate
|
|
182
|
-
// This path is for backward compat before per-agent daily tracking was added
|
|
183
|
-
}
|
|
184
|
-
return total;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getAgentErrorRate(agentId) {
|
|
188
|
-
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
189
|
-
const metrics = safeJson(metricsPath) || {};
|
|
190
|
-
const m = metrics[agentId];
|
|
191
|
-
if (!m) return 0;
|
|
192
|
-
const total = m.tasksCompleted + m.tasksErrored;
|
|
193
|
-
return total > 0 ? m.tasksErrored / total : 0;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function isAgentIdle(agentId) {
|
|
197
|
-
// Dispatch queue is the single source of truth for agent availability
|
|
198
|
-
const dispatch = safeJson(DISPATCH_PATH) || {};
|
|
199
|
-
return !(dispatch.active || []).some(d => d.agent === agentId);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Track agents claimed during a single discovery pass to distribute work
|
|
203
|
-
const _claimedAgents = new Set();
|
|
204
|
-
function resetClaimedAgents() { _claimedAgents.clear(); }
|
|
205
|
-
|
|
206
|
-
function resolveAgent(workType, config, authorAgent = null) {
|
|
207
|
-
const routes = getRoutingTableCached();
|
|
208
|
-
const route = routes[workType] || routes['implement'];
|
|
209
|
-
const agents = config.agents || {};
|
|
210
|
-
|
|
211
|
-
// Resolve _author_ token
|
|
212
|
-
let preferred = route.preferred === '_author_' ? authorAgent : route.preferred;
|
|
213
|
-
let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
|
|
214
|
-
|
|
215
|
-
const isAvailable = (id) => {
|
|
216
|
-
if (!agents[id] || !isAgentIdle(id) || _claimedAgents.has(id)) return false;
|
|
217
|
-
// Budget check — no budget means infinite (no limit)
|
|
218
|
-
const budget = agents[id].monthlyBudgetUsd;
|
|
219
|
-
if (budget && budget > 0) {
|
|
220
|
-
if (getMonthlySpend(id) >= budget) return false;
|
|
221
|
-
}
|
|
222
|
-
return true;
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// Check preferred and fallback first (routing table order)
|
|
226
|
-
if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
|
|
227
|
-
if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
|
|
228
|
-
|
|
229
|
-
// Fall back to any idle agent, preferring lower error rates
|
|
230
|
-
const idle = Object.keys(agents)
|
|
231
|
-
.filter(id => id !== preferred && id !== fallback && isAvailable(id))
|
|
232
|
-
.sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
|
|
233
|
-
|
|
234
|
-
if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
|
|
235
|
-
|
|
236
|
-
// No idle configured agent — try temp agent if enabled
|
|
237
|
-
if (config.engine?.allowTempAgents) {
|
|
238
|
-
const tempId = `temp-${shared.uid()}`;
|
|
239
|
-
_claimedAgents.add(tempId);
|
|
240
|
-
tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: ts() });
|
|
241
|
-
log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
|
|
242
|
-
return tempId;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// No idle agent available — return null, item stays pending until next tick
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ─── Task Context Resolution ────────────────────────────────────────────────
|
|
250
|
-
// Resolves implicit references in task descriptions (e.g., "ripley's plan",
|
|
251
|
-
// "dallas's PR") to actual artifacts and injects their content.
|
|
252
|
-
|
|
253
|
-
function resolveTaskContext(item, config) {
|
|
254
|
-
const title = (item.title || '').toLowerCase();
|
|
255
|
-
const desc = (item.description || '').toLowerCase();
|
|
256
|
-
const text = title + ' ' + desc;
|
|
257
|
-
const agentNames = Object.entries(config.agents || {}).map(([id, a]) => ({
|
|
258
|
-
id,
|
|
259
|
-
name: (a.name || id).toLowerCase(),
|
|
260
|
-
}));
|
|
261
|
-
const resolved = { additionalContext: '', referencedFiles: [] };
|
|
262
|
-
|
|
263
|
-
// Match agent references: "ripley's plan", "dallas's pr", "lambert's output", etc.
|
|
264
|
-
for (const agent of agentNames) {
|
|
265
|
-
const patterns = [
|
|
266
|
-
new RegExp(`${agent.name}(?:'s|s)?\\s+plan`, 'i'),
|
|
267
|
-
new RegExp(`${agent.id}(?:'s|s)?\\s+plan`, 'i'),
|
|
268
|
-
new RegExp(`plan\\s+(?:created|made|written|generated)\\s+by\\s+${agent.name}`, 'i'),
|
|
269
|
-
new RegExp(`plan\\s+(?:created|made|written|generated)\\s+by\\s+${agent.id}`, 'i'),
|
|
270
|
-
];
|
|
271
|
-
const matchesPlan = patterns.some(p => p.test(text));
|
|
272
|
-
if (matchesPlan) {
|
|
273
|
-
// Find plans created by this agent (check work items for plan tasks dispatched to this agent)
|
|
274
|
-
try {
|
|
275
|
-
const plans = fs.readdirSync(path.join(MINIONS_DIR, 'plans')).filter(f => f.endsWith('.md') || f.endsWith('.json'));
|
|
276
|
-
// Check work-items to find which plan file this agent created
|
|
277
|
-
const workItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
278
|
-
const agentPlanItems = workItems.filter(w =>
|
|
279
|
-
w.type === 'plan' && w.dispatched_to === agent.id && w.status === 'done' && w._planFileName
|
|
280
|
-
).sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
|
|
281
|
-
|
|
282
|
-
if (agentPlanItems.length > 0) {
|
|
283
|
-
const planFile = agentPlanItems[0]._planFileName;
|
|
284
|
-
const planPath = path.join(MINIONS_DIR, 'plans', planFile);
|
|
285
|
-
try {
|
|
286
|
-
const content = safeRead(planPath);
|
|
287
|
-
resolved.additionalContext += `\n\n## Referenced Plan: ${planFile} (created by ${agent.name})\n\n${content}`;
|
|
288
|
-
resolved.referencedFiles.push(planPath);
|
|
289
|
-
log('info', `Context resolution: found plan "${planFile}" by ${agent.name} for work item ${item.id}`);
|
|
290
|
-
} catch {}
|
|
291
|
-
} else if (plans.length > 0) {
|
|
292
|
-
// Fallback: try to find a plan file with the agent's name or ID in it
|
|
293
|
-
const match = plans.find(f => f.toLowerCase().includes(agent.id) || f.toLowerCase().includes(agent.name));
|
|
294
|
-
if (match) {
|
|
295
|
-
const planPath = path.join(MINIONS_DIR, 'plans', match);
|
|
296
|
-
try {
|
|
297
|
-
const content = safeRead(planPath);
|
|
298
|
-
resolved.additionalContext += `\n\n## Referenced Plan: ${match}\n\n${content}`;
|
|
299
|
-
resolved.referencedFiles.push(planPath);
|
|
300
|
-
log('info', `Context resolution: found plan "${match}" (name match) for work item ${item.id}`);
|
|
301
|
-
} catch {}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
} catch {}
|
|
305
|
-
}
|
|
126
|
+
// ─── Routing (extracted to engine/routing.js) ───────────────────────────────
|
|
306
127
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
new RegExp(`(?:output|findings|notes|results)\\s+(?:from|by)\\s+${agent.name}`, 'i'),
|
|
311
|
-
];
|
|
312
|
-
if (outputPatterns.some(p => p.test(text))) {
|
|
313
|
-
// Find the agent's latest inbox notes
|
|
314
|
-
try {
|
|
315
|
-
const inboxDir = path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
316
|
-
const files = fs.readdirSync(inboxDir)
|
|
317
|
-
.filter(f => f.startsWith(agent.id + '-'))
|
|
318
|
-
.sort().reverse();
|
|
319
|
-
if (files.length > 0) {
|
|
320
|
-
const content = safeRead(path.join(inboxDir, files[0]));
|
|
321
|
-
resolved.additionalContext += `\n\n## Referenced Notes by ${agent.name}: ${files[0]}\n\n${content.slice(0, 5000)}`;
|
|
322
|
-
resolved.referencedFiles.push(path.join(inboxDir, files[0]));
|
|
323
|
-
log('info', `Context resolution: found notes "${files[0]}" by ${agent.name} for work item ${item.id}`);
|
|
324
|
-
}
|
|
325
|
-
} catch {}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
128
|
+
const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
129
|
+
getAgentErrorRate, isAgentIdle, resolveAgent, resetClaimedAgents,
|
|
130
|
+
tempAgents } = require('./engine/routing');
|
|
328
131
|
|
|
329
|
-
|
|
330
|
-
// find the most recent plan
|
|
331
|
-
if (!resolved.additionalContext && /\b(the|latest|last|recent)\s+plan\b/i.test(text)) {
|
|
332
|
-
try {
|
|
333
|
-
const plans = fs.readdirSync(path.join(MINIONS_DIR, 'plans'))
|
|
334
|
-
.filter(f => f.endsWith('.md') || f.endsWith('.json'))
|
|
335
|
-
.sort().reverse();
|
|
336
|
-
if (plans.length > 0) {
|
|
337
|
-
const planPath = path.join(MINIONS_DIR, 'plans', plans[0]);
|
|
338
|
-
const content = safeRead(planPath);
|
|
339
|
-
resolved.additionalContext += `\n\n## Referenced Plan (latest): ${plans[0]}\n\n${content}`;
|
|
340
|
-
resolved.referencedFiles.push(planPath);
|
|
341
|
-
log('info', `Context resolution: using latest plan "${plans[0]}" for work item ${item.id}`);
|
|
342
|
-
}
|
|
343
|
-
} catch {}
|
|
344
|
-
}
|
|
132
|
+
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
345
133
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// ─── Playbook Renderer ──────────────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
function renderPlaybook(type, vars) {
|
|
352
|
-
const pbPath = path.join(PLAYBOOKS_DIR, `${type}.md`);
|
|
353
|
-
let content;
|
|
354
|
-
try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
|
|
355
|
-
log('warn', `Playbook not found: ${type}`);
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Inject pinned context (always visible to agents) — capped at 4KB
|
|
360
|
-
let pinnedContent = '';
|
|
361
|
-
try { pinnedContent = fs.readFileSync(path.join(MINIONS_DIR, 'pinned.md'), 'utf8'); } catch {}
|
|
362
|
-
if (pinnedContent) {
|
|
363
|
-
if (pinnedContent.length > 4096) pinnedContent = pinnedContent.slice(0, 4096) + '\n\n_...pinned.md truncated (read full file if needed)_';
|
|
364
|
-
content += '\n\n---\n\n## Pinned Context (CRITICAL — READ FIRST)\n\n' + pinnedContent;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Inject team notes (single injection point — not in buildAgentContext) — capped at 8KB
|
|
368
|
-
let notes = getNotes();
|
|
369
|
-
if (notes) {
|
|
370
|
-
if (notes.length > 8192) {
|
|
371
|
-
const sections = notes.split(/(?=^### )/m);
|
|
372
|
-
const recent = sections.slice(-10).join('');
|
|
373
|
-
notes = recent.length > 8192 ? recent.slice(0, 8192) + '\n\n_...notes truncated_' : recent;
|
|
374
|
-
notes += '\n\n_' + Math.max(0, sections.length - 10) + ' older entries in `notes.md` — Read if needed._';
|
|
375
|
-
}
|
|
376
|
-
content += '\n\n---\n\n## Team Notes (MUST READ)\n\n' + notes;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Inject KB guardrail
|
|
380
|
-
content += `\n\n---\n\n## Knowledge Base Rules\n\n`;
|
|
381
|
-
content += `**Never delete, move, or overwrite files in \`knowledge/\`.** The sweep (consolidation engine) is the only process that writes to \`knowledge/\`. If you think a KB file is wrong, note it in your learnings file — do not touch \`knowledge/\` directly.\n`;
|
|
382
|
-
|
|
383
|
-
// Inject learnings requirement
|
|
384
|
-
content += `\n\n---\n\n## REQUIRED: Write Learnings\n\n`;
|
|
385
|
-
content += `After completing your task, you MUST write a findings/learnings file to:\n`;
|
|
386
|
-
content += `\`${MINIONS_DIR}/notes/inbox/${vars.agent_id || 'agent'}-${dateStamp()}.md\`\n\n`;
|
|
387
|
-
content += `Include:\n`;
|
|
388
|
-
content += `- What you learned about the codebase\n`;
|
|
389
|
-
content += `- Patterns you discovered or established\n`;
|
|
390
|
-
content += `- Gotchas or warnings for future agents\n`;
|
|
391
|
-
content += `- Conventions to follow\n`;
|
|
392
|
-
content += `- **SOURCE REFERENCES for every finding** — file paths with line numbers, PR URLs, API endpoints, config keys. Format: \`(source: path/to/file.ts:42)\` or \`(source: PR-12345)\`. Without references, findings cannot be verified.\n\n`;
|
|
393
|
-
content += `### Skill Extraction (IMPORTANT)\n\n`;
|
|
394
|
-
content += `If during this task you discovered a **repeatable workflow** — a multi-step procedure, workaround, build process, or pattern that other agents should follow in similar situations — output it as a fenced skill block. The engine will automatically extract it.\n\n`;
|
|
395
|
-
content += `Format your skill as a fenced code block with the \`skill\` language tag:\n\n`;
|
|
396
|
-
content += '````\n```skill\n';
|
|
397
|
-
content += `---\nname: short-descriptive-name\ndescription: One-line description of what this skill does\nallowed-tools: Bash, Read, Edit\ntrigger: when should an agent use this\nscope: minions\nproject: any\n---\n\n# Skill Title\n\n## Steps\n1. ...\n2. ...\n\n## Notes\n...\n`;
|
|
398
|
-
content += '```\n````\n\n';
|
|
399
|
-
content += `- Set \`scope: minions\` for cross-project skills (engine writes to ~/.claude/skills/ automatically)\n`;
|
|
400
|
-
content += `- Set \`scope: project\` + \`project: <name>\` for repo-specific skills (engine queues a PR to <project>/.claude/skills/)\n`;
|
|
401
|
-
content += `- Only output a skill block if you genuinely discovered something reusable — don't force it\n`;
|
|
402
|
-
|
|
403
|
-
// Inject project-level variables from config
|
|
404
|
-
const config = getConfig();
|
|
405
|
-
const projects = getProjects(config);
|
|
406
|
-
// Find the specific project being dispatched (match by repo_id or repo_name from vars)
|
|
407
|
-
const dispatchProject = (vars.repo_id && projects.find(p => p.repositoryId === vars.repo_id))
|
|
408
|
-
|| (vars.repo_name && projects.find(p => p.repoName === vars.repo_name))
|
|
409
|
-
|| projects[0] || {};
|
|
410
|
-
const projectVars = {
|
|
411
|
-
project_name: dispatchProject.name || 'Unknown Project',
|
|
412
|
-
ado_org: dispatchProject.adoOrg || 'Unknown',
|
|
413
|
-
ado_project: dispatchProject.adoProject || 'Unknown',
|
|
414
|
-
repo_name: dispatchProject.repoName || 'Unknown',
|
|
415
|
-
pr_create_instructions: getPrCreateInstructions(dispatchProject),
|
|
416
|
-
pr_comment_instructions: getPrCommentInstructions(dispatchProject),
|
|
417
|
-
pr_fetch_instructions: getPrFetchInstructions(dispatchProject),
|
|
418
|
-
pr_vote_instructions: getPrVoteInstructions(dispatchProject),
|
|
419
|
-
repo_host_label: getRepoHostLabel(dispatchProject),
|
|
420
|
-
};
|
|
421
|
-
const allVars = { ...projectVars, ...vars };
|
|
422
|
-
|
|
423
|
-
// Substitute variables
|
|
424
|
-
for (const [key, val] of Object.entries(allVars)) {
|
|
425
|
-
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(val));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return content;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// ─── Repo Host Helpers ──────────────────────────────────────────────────────
|
|
432
|
-
|
|
433
|
-
function getRepoHost(project) {
|
|
434
|
-
return project?.repoHost || 'ado';
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function getPrCreateInstructions(project) {
|
|
438
|
-
const host = getRepoHost(project);
|
|
439
|
-
const repoId = project?.repositoryId || '';
|
|
440
|
-
if (host === 'github') {
|
|
441
|
-
const org = project?.adoOrg || '';
|
|
442
|
-
const repo = project?.repoName || '';
|
|
443
|
-
const mainBranch = project?.mainBranch || 'main';
|
|
444
|
-
return `Use \`gh pr create\` to create a pull request:\n` +
|
|
445
|
-
`- \`gh pr create --base ${mainBranch} --head <your-branch> --title "PR title" --body "PR description" --repo ${org}/${repo}\`\n` +
|
|
446
|
-
`- Always set --base to \`${mainBranch}\` (the main branch)\n` +
|
|
447
|
-
`- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
|
|
448
|
-
`- Use --head to specify your feature branch name\n` +
|
|
449
|
-
`- Include a meaningful --title and --body describing the changes`;
|
|
450
|
-
}
|
|
451
|
-
// Default: Azure DevOps
|
|
452
|
-
return `Use \`mcp__azure-ado__repo_create_pull_request\`:\n- repositoryId: \`${repoId}\``;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function getPrCommentInstructions(project) {
|
|
456
|
-
const host = getRepoHost(project);
|
|
457
|
-
const repoId = project?.repositoryId || '';
|
|
458
|
-
if (host === 'github') {
|
|
459
|
-
const org = project?.adoOrg || '';
|
|
460
|
-
const repo = project?.repoName || '';
|
|
461
|
-
return `Use \`gh pr comment\` to post a comment on the PR:\n` +
|
|
462
|
-
`- \`gh pr comment <number> --body "Your comment text" --repo ${org}/${repo}\`\n` +
|
|
463
|
-
`- Replace <number> with the PR number\n` +
|
|
464
|
-
`- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
|
|
465
|
-
`- Use --body to provide the comment text (supports Markdown)`;
|
|
466
|
-
}
|
|
467
|
-
return `Use \`mcp__azure-ado__repo_create_pull_request_thread\`:\n- repositoryId: \`${repoId}\``;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function getPrFetchInstructions(project) {
|
|
471
|
-
const host = getRepoHost(project);
|
|
472
|
-
if (host === 'github') {
|
|
473
|
-
const org = project?.adoOrg || '';
|
|
474
|
-
const repo = project?.repoName || '';
|
|
475
|
-
const mainBranch = project?.mainBranch || 'main';
|
|
476
|
-
return `Use \`gh pr view\` to fetch PR status:\n` +
|
|
477
|
-
`- \`gh pr view <number> --json number,title,state,mergeable,reviewDecision,headRefName,baseRefName,statusCheckRollup --repo ${org}/${repo}\`\n` +
|
|
478
|
-
`- This returns JSON with PR state, mergeability, review decision, and check statuses\n` +
|
|
479
|
-
`- To fetch the PR branch locally:\n` +
|
|
480
|
-
` 1. \`git fetch origin <branch-name>\`\n` +
|
|
481
|
-
` 2. \`git checkout <branch-name>\`\n` +
|
|
482
|
-
`- Or use \`gh pr checkout <number> --repo ${org}/${repo}\` to fetch and checkout in one step\n` +
|
|
483
|
-
`- The base branch is \`${mainBranch}\``;
|
|
484
|
-
}
|
|
485
|
-
return `Use \`mcp__azure-ado__repo_get_pull_request_by_id\` to fetch PR status.`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function getPrVoteInstructions(project) {
|
|
489
|
-
const host = getRepoHost(project);
|
|
490
|
-
const repoId = project?.repositoryId || '';
|
|
491
|
-
if (host === 'github') {
|
|
492
|
-
const org = project?.adoOrg || '';
|
|
493
|
-
const repo = project?.repoName || '';
|
|
494
|
-
return `Use \`gh pr review\` to submit a review on the PR:\n` +
|
|
495
|
-
`- Approve: \`gh pr review <number> --approve --body "Approval comment" --repo ${org}/${repo}\`\n` +
|
|
496
|
-
`- Request changes: \`gh pr review <number> --request-changes --body "What needs to change" --repo ${org}/${repo}\`\n` +
|
|
497
|
-
`- Comment only: \`gh pr review <number> --comment --body "Review comment" --repo ${org}/${repo}\`\n` +
|
|
498
|
-
`- Replace <number> with the PR number\n` +
|
|
499
|
-
`- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
|
|
500
|
-
`- Use --body to provide a review summary (supports Markdown)`;
|
|
501
|
-
}
|
|
502
|
-
return `Use \`mcp__azure-ado__repo_update_pull_request_reviewers\`:\n- repositoryId: \`${repoId}\`\n- Set your reviewer vote on the PR (10=approve, 5=approve-with-suggestions, -10=reject)`;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function getRepoHostLabel(project) {
|
|
506
|
-
const host = getRepoHost(project);
|
|
507
|
-
if (host === 'github') return 'GitHub';
|
|
508
|
-
return 'Azure DevOps';
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function getRepoHostToolRule(project) {
|
|
512
|
-
const host = getRepoHost(project);
|
|
513
|
-
if (host === 'github') return 'Use GitHub MCP tools or `gh` CLI for PR operations';
|
|
514
|
-
return 'Use Azure DevOps MCP tools (mcp__azure-ado__*) for PR operations — NEVER use gh CLI';
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// ─── System Prompt Builder ──────────────────────────────────────────────────
|
|
518
|
-
|
|
519
|
-
// Lean system prompt: agent identity + rules only (~2-4KB, never grows)
|
|
520
|
-
function buildSystemPrompt(agentId, config, project) {
|
|
521
|
-
const agent = config.agents[agentId] || tempAgents.get(agentId) || { name: agentId, role: 'Temporary Agent', skills: [] };
|
|
522
|
-
const charter = getAgentCharter(agentId); // returns '' for temp agents (no charter file)
|
|
523
|
-
project = project || getProjects(config)[0] || {};
|
|
524
|
-
|
|
525
|
-
let prompt = '';
|
|
526
|
-
|
|
527
|
-
// Agent identity
|
|
528
|
-
prompt += `# You are ${agent.name} (${agent.role})\n\n`;
|
|
529
|
-
prompt += `Agent ID: ${agentId}\n`;
|
|
530
|
-
prompt += `Skills: ${(agent.skills || []).join(', ')}\n\n`;
|
|
531
|
-
|
|
532
|
-
// Charter (detailed instructions — typically 1-2KB)
|
|
533
|
-
if (charter) {
|
|
534
|
-
prompt += `## Your Charter\n\n${charter}\n\n`;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Project context (fixed size)
|
|
538
|
-
prompt += `## Project: ${project.name || 'Unknown Project'}\n\n`;
|
|
539
|
-
prompt += `- Repo: ${project.repoName || 'Unknown'} (${project.adoOrg || 'Unknown'}/${project.adoProject || 'Unknown'})\n`;
|
|
540
|
-
prompt += `- Repo ID: ${project.repositoryId || ''}\n`;
|
|
541
|
-
prompt += `- Repo host: ${getRepoHostLabel(project)}\n`;
|
|
542
|
-
prompt += `- Main branch: ${project.mainBranch || 'main'}\n\n`;
|
|
543
|
-
|
|
544
|
-
// Critical rules (fixed size)
|
|
545
|
-
prompt += `## Critical Rules\n\n`;
|
|
546
|
-
prompt += `1. Use git worktrees — NEVER checkout on main working tree\n`;
|
|
547
|
-
prompt += `2. ${getRepoHostToolRule(project)}\n`;
|
|
548
|
-
prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
|
|
549
|
-
prompt += `4. Write learnings to: ${MINIONS_DIR}/notes/inbox/${agentId}-${dateStamp()}.md\n`;
|
|
550
|
-
prompt += `5. Agent status is managed by the engine via dispatch.json — agents do not need to track their own status\n`;
|
|
551
|
-
prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — the engine auto-extracts it to ~/.claude/skills/\n\n`;
|
|
552
|
-
|
|
553
|
-
return prompt;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Bulk context: history, notes, conventions, skills — prepended to user/task prompt.
|
|
557
|
-
// This is the content that grows over time and would bloat the system prompt.
|
|
558
|
-
function buildAgentContext(agentId, config, project) {
|
|
559
|
-
project = project || getProjects(config)[0] || {};
|
|
560
|
-
let context = '';
|
|
561
|
-
|
|
562
|
-
// Agent history — last 5 tasks only (keeps it relevant, avoids 37KB dumps)
|
|
563
|
-
const history = safeRead(path.join(AGENTS_DIR, agentId, 'history.md'));
|
|
564
|
-
if (history && history.trim() !== '# Agent History') {
|
|
565
|
-
const entries = history.split(/(?=^### )/m);
|
|
566
|
-
const header = entries[0].startsWith('#') && !entries[0].startsWith('### ') ? entries.shift() : '';
|
|
567
|
-
const recent = entries.slice(-5);
|
|
568
|
-
const trimmed = (header ? header + '\n' : '') + recent.join('');
|
|
569
|
-
context += `## Your Recent History (last 5 tasks)\n\n${trimmed}\n\n`;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Project conventions (from CLAUDE.md) — always relevant for code quality
|
|
573
|
-
if (project.localPath) {
|
|
574
|
-
const claudeMd = safeRead(path.join(project.localPath, 'CLAUDE.md'));
|
|
575
|
-
if (claudeMd && claudeMd.trim()) {
|
|
576
|
-
const truncated = claudeMd.length > 8192 ? claudeMd.slice(0, 8192) + '\n\n...(truncated)' : claudeMd;
|
|
577
|
-
context += `## Project Conventions (from CLAUDE.md)\n\n${truncated}\n\n`;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// KB and skills: NOT injected — agents can Glob/Read when needed
|
|
582
|
-
// This saves ~27KB per dispatch. Reference note so agents know they exist:
|
|
583
|
-
context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`. Skills are in \`skills/*.md\` and \`.claude/skills/\`. Use Glob/Read to browse when relevant.\n\n`;
|
|
584
|
-
|
|
585
|
-
// Minions awareness: what's in flight, who's doing what
|
|
586
|
-
const dispatch = getDispatch();
|
|
587
|
-
const activeItems = (dispatch.active || []).map(d =>
|
|
588
|
-
`- **${d.agent}**: ${d.type} — ${(d.task || '').slice(0, 100)}${d.agent === agentId ? ' ← (you)' : ''}`
|
|
589
|
-
);
|
|
590
|
-
if (activeItems.length > 0) {
|
|
591
|
-
context += `## Active Agents\n\n${activeItems.join('\n')}\n\n`;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Recent completions (last 5, not 10)
|
|
595
|
-
const recentCompleted = (dispatch.completed || []).slice(-5).reverse().map(d =>
|
|
596
|
-
`- **${d.agent}** ${d.result === 'success' ? 'completed' : 'failed'}: ${(d.task || '').slice(0, 80)}${d.resultSummary ? ' — ' + d.resultSummary.slice(0, 100) : ''}`
|
|
597
|
-
);
|
|
598
|
-
if (recentCompleted.length > 0) {
|
|
599
|
-
context += `## Recently Completed\n\n${recentCompleted.join('\n')}\n\n`;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Active + linked PRs across projects — coordination awareness
|
|
603
|
-
const projects = getProjects(config);
|
|
604
|
-
const allPrs = [];
|
|
605
|
-
for (const p of projects) {
|
|
606
|
-
const prs = getPrs(p).filter(pr => pr.status === 'active' || pr.status === 'linked');
|
|
607
|
-
for (const pr of prs) allPrs.push({ ...pr, _project: p.name });
|
|
608
|
-
}
|
|
609
|
-
// Also check central pull-requests.json
|
|
610
|
-
try {
|
|
611
|
-
const centralPrs = safeJson(path.join(MINIONS_DIR, 'pull-requests.json')) || [];
|
|
612
|
-
for (const pr of centralPrs.filter(pr => pr.status === 'active' || pr.status === 'linked')) {
|
|
613
|
-
if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
|
|
614
|
-
}
|
|
615
|
-
} catch {}
|
|
616
|
-
if (allPrs.length > 0) {
|
|
617
|
-
const prLines = allPrs.map(pr =>
|
|
618
|
-
`- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${pr.status === 'linked' ? 'context-only' : (pr.reviewStatus || 'pending')}${pr.buildStatus === 'failing' ? ', BUILD FAILING' : ''}]${pr.branch ? ' branch: `' + pr.branch + '`' : ''}${pr._context ? ' — ' + pr._context.slice(0, 100) : ''}`
|
|
619
|
-
);
|
|
620
|
-
context += `## Active Pull Requests\n\n${prLines.join('\n')}\n\n`;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Pending work items
|
|
624
|
-
const pendingItems = (dispatch.pending || []).slice(0, 10).map(d =>
|
|
625
|
-
`- ${d.type}: ${(d.task || '').slice(0, 80)}`
|
|
626
|
-
);
|
|
627
|
-
if (pendingItems.length > 0) {
|
|
628
|
-
context += `## Pending Work Queue (${(dispatch.pending || []).length} items)\n\n${pendingItems.join('\n')}${(dispatch.pending || []).length > 10 ? '\n- ... and ' + ((dispatch.pending || []).length - 10) + ' more' : ''}\n\n`;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Team notes injected via renderPlaybook (single injection point, with truncation)
|
|
632
|
-
// Not duplicated here to avoid double-injection and token waste
|
|
633
|
-
|
|
634
|
-
return context;
|
|
635
|
-
}
|
|
134
|
+
const { renderPlaybook, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
135
|
+
buildBaseVars, buildPrDispatch, resolveTaskContext,
|
|
136
|
+
getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
|
|
636
137
|
|
|
637
138
|
// sanitizeBranch imported from shared.js
|
|
638
139
|
|
|
@@ -645,7 +146,7 @@ const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, handleP
|
|
|
645
146
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
646
147
|
|
|
647
148
|
const activeProcesses = new Map(); // dispatchId → { proc, agentId, startedAt }
|
|
648
|
-
|
|
149
|
+
// tempAgents imported from engine/routing.js
|
|
649
150
|
let engineRestartGraceUntil = 0; // timestamp — suppress orphan detection until this time
|
|
650
151
|
|
|
651
152
|
// Resolve dependency plan item IDs to their PR branches
|
|
@@ -703,7 +204,7 @@ function findExistingWorktree(repoDir, branchName) {
|
|
|
703
204
|
}
|
|
704
205
|
}
|
|
705
206
|
}
|
|
706
|
-
} catch {}
|
|
207
|
+
} catch (e) { log('warn', 'git: ' + e.message); }
|
|
707
208
|
return null;
|
|
708
209
|
}
|
|
709
210
|
|
|
@@ -727,7 +228,7 @@ function removeStaleIndexLock(rootDir) {
|
|
|
727
228
|
log('warn', `Removed stale index.lock (${Math.round(age / 1000)}s old) in ${rootDir}`);
|
|
728
229
|
}
|
|
729
230
|
}
|
|
730
|
-
} catch {}
|
|
231
|
+
} catch (e) { log('warn', 'git: ' + e.message); }
|
|
731
232
|
}
|
|
732
233
|
|
|
733
234
|
function runWorktreeAdd(rootDir, worktreePath, args, gitOpts, worktreeCreateRetries) {
|
|
@@ -736,7 +237,7 @@ function runWorktreeAdd(rootDir, worktreePath, args, gitOpts, worktreeCreateRetr
|
|
|
736
237
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
737
238
|
try {
|
|
738
239
|
if (attempt > 0) {
|
|
739
|
-
try { exec('git worktree prune', { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch {}
|
|
240
|
+
try { exec('git worktree prune', { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
740
241
|
removeStaleIndexLock(rootDir);
|
|
741
242
|
log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
|
|
742
243
|
}
|
|
@@ -795,8 +296,8 @@ function spawnAgent(dispatchItem, config) {
|
|
|
795
296
|
if (existingWt) {
|
|
796
297
|
worktreePath = existingWt;
|
|
797
298
|
log('info', `Reusing existing worktree for ${branchName}: ${existingWt}`);
|
|
798
|
-
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
|
|
799
|
-
try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: existingWt }); } catch {}
|
|
299
|
+
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
300
|
+
try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: existingWt }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
800
301
|
} else if (type !== 'implement') {
|
|
801
302
|
// Only implement tasks may create new worktrees.
|
|
802
303
|
// Other task types are reuse-only: if no existing worktree, run in rootDir.
|
|
@@ -808,13 +309,13 @@ function spawnAgent(dispatchItem, config) {
|
|
|
808
309
|
if (!fs.existsSync(worktreePath)) {
|
|
809
310
|
const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
|
|
810
311
|
// Prune stale worktree entries before creating (handles leftover entries from crashed runs)
|
|
811
|
-
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
|
|
312
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
812
313
|
// Remove stale index.lock before creating worktree (Windows crashes can leave this behind)
|
|
813
314
|
removeStaleIndexLock(rootDir);
|
|
814
315
|
|
|
815
316
|
if (isSharedBranch) {
|
|
816
317
|
log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
|
|
817
|
-
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
|
|
318
|
+
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
818
319
|
try {
|
|
819
320
|
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
820
321
|
} catch (eShared) {
|
|
@@ -832,7 +333,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
832
333
|
runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${sanitizeBranch(project.mainBranch || 'main')}`, _worktreeGitOpts, worktreeCreateRetries);
|
|
833
334
|
} catch (e1) {
|
|
834
335
|
// Branch already exists or checked out elsewhere — try without -b
|
|
835
|
-
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
|
|
336
|
+
try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
836
337
|
try {
|
|
837
338
|
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
838
339
|
log('info', `Reusing existing branch: ${branchName}`);
|
|
@@ -859,12 +360,12 @@ function spawnAgent(dispatchItem, config) {
|
|
|
859
360
|
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
860
361
|
// Directory gone but git still tracks it — prune and recreate
|
|
861
362
|
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
862
|
-
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
|
|
363
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
863
364
|
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
864
365
|
log('info', `Recovered worktree for ${branchName} after stale entry prune`);
|
|
865
366
|
} else {
|
|
866
367
|
// Can't find the worktree at all — prune and retry
|
|
867
|
-
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
|
|
368
|
+
try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
868
369
|
runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
|
|
869
370
|
}
|
|
870
371
|
} else {
|
|
@@ -875,7 +376,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
875
376
|
}
|
|
876
377
|
} else if (meta?.branchStrategy === 'shared-branch') {
|
|
877
378
|
log('info', `Pulling latest on shared branch ${branchName}`);
|
|
878
|
-
try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: worktreePath }); } catch {}
|
|
379
|
+
try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: worktreePath }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
879
380
|
}
|
|
880
381
|
} catch (err) {
|
|
881
382
|
if (recoverPartialWorktree(rootDir, worktreePath, branchName, _gitOpts)) {
|
|
@@ -956,7 +457,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
956
457
|
log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
|
|
957
458
|
}
|
|
958
459
|
}
|
|
959
|
-
} catch {}
|
|
460
|
+
} catch (e) { log('warn', 'session resume lookup: ' + e.message); }
|
|
960
461
|
}
|
|
961
462
|
|
|
962
463
|
// MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
|
|
@@ -997,14 +498,14 @@ function spawnAgent(dispatchItem, config) {
|
|
|
997
498
|
const silentMs = Date.now() - lastOutputAt;
|
|
998
499
|
if (silentMs < 30000) return;
|
|
999
500
|
const silentSec = Math.round(silentMs / 1000);
|
|
1000
|
-
try { fs.appendFileSync(liveOutputPath, `[heartbeat] running — no output for ${silentSec}s\n`); } catch {}
|
|
501
|
+
try { fs.appendFileSync(liveOutputPath, `[heartbeat] running — no output for ${silentSec}s\n`); } catch { /* optional */ }
|
|
1001
502
|
}, 30000);
|
|
1002
503
|
|
|
1003
504
|
proc.stdout.on('data', (data) => {
|
|
1004
505
|
const chunk = data.toString();
|
|
1005
506
|
lastOutputAt = Date.now();
|
|
1006
507
|
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1007
|
-
try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
|
|
508
|
+
try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
|
|
1008
509
|
|
|
1009
510
|
// Capture sessionId early for mid-session steering
|
|
1010
511
|
const procInfo = activeProcesses.get(id);
|
|
@@ -1021,7 +522,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1021
522
|
break;
|
|
1022
523
|
}
|
|
1023
524
|
}
|
|
1024
|
-
} catch {}
|
|
525
|
+
} catch { /* JSON parse — output may not be valid JSON */ }
|
|
1025
526
|
}
|
|
1026
527
|
});
|
|
1027
528
|
|
|
@@ -1029,7 +530,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1029
530
|
const chunk = data.toString();
|
|
1030
531
|
lastOutputAt = Date.now();
|
|
1031
532
|
if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
|
|
1032
|
-
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
|
|
533
|
+
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
|
|
1033
534
|
});
|
|
1034
535
|
|
|
1035
536
|
function onAgentClose(code) {
|
|
@@ -1077,13 +578,13 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1077
578
|
const chunk = data.toString();
|
|
1078
579
|
lastOutputAt = Date.now();
|
|
1079
580
|
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1080
|
-
try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
|
|
581
|
+
try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
|
|
1081
582
|
});
|
|
1082
583
|
resumeProc.stderr.on('data', (data) => {
|
|
1083
584
|
const chunk = data.toString();
|
|
1084
585
|
lastOutputAt = Date.now();
|
|
1085
586
|
if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
|
|
1086
|
-
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
|
|
587
|
+
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
|
|
1087
588
|
});
|
|
1088
589
|
|
|
1089
590
|
// Re-wire close handler for the resumed process
|
|
@@ -1106,9 +607,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1106
607
|
const stillActive = (dispatchNow.active || []).some(d => d.id === id);
|
|
1107
608
|
if (!stillActive) {
|
|
1108
609
|
log('info', `Agent ${agentId} (${id}) close event ignored — dispatch already completed elsewhere`);
|
|
1109
|
-
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
1110
|
-
try { fs.unlinkSync(promptPath); } catch {}
|
|
1111
|
-
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
610
|
+
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
611
|
+
try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
|
|
612
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
|
|
1112
613
|
return;
|
|
1113
614
|
}
|
|
1114
615
|
|
|
@@ -1124,9 +625,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1124
625
|
const errMsg = stderr.includes('claude-code') ? stderr.trim() : 'Configuration error — Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code';
|
|
1125
626
|
log('error', `Agent ${agentId} (${id}) failed: ${errMsg}`);
|
|
1126
627
|
completeDispatch(id, 'error', errMsg, '');
|
|
1127
|
-
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
1128
|
-
try { fs.unlinkSync(promptPath); } catch {}
|
|
1129
|
-
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
628
|
+
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
629
|
+
try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
|
|
630
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
|
|
1130
631
|
return;
|
|
1131
632
|
}
|
|
1132
633
|
|
|
@@ -1137,9 +638,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1137
638
|
completeDispatch(id, code === 0 ? 'success' : 'error', '', resultSummary);
|
|
1138
639
|
|
|
1139
640
|
// Cleanup temp files (including PID file now that dispatch is complete)
|
|
1140
|
-
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
1141
|
-
try { fs.unlinkSync(promptPath); } catch {}
|
|
1142
|
-
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
641
|
+
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
642
|
+
try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
|
|
643
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
|
|
1143
644
|
|
|
1144
645
|
log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
|
|
1145
646
|
|
|
@@ -1151,7 +652,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1151
652
|
// Keep output archive but remove temp agent directory (live-output.log etc.)
|
|
1152
653
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
1153
654
|
log('info', `Temp agent ${agentId} cleaned up`);
|
|
1154
|
-
} catch {}
|
|
655
|
+
} catch { /* cleanup */ }
|
|
1155
656
|
}
|
|
1156
657
|
}
|
|
1157
658
|
|
|
@@ -1287,7 +788,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
1287
788
|
const wi = items.find(i => i.id === item.meta.item.id);
|
|
1288
789
|
if (wi) retries = wi._retryCount || 0;
|
|
1289
790
|
}
|
|
1290
|
-
} catch {}
|
|
791
|
+
} catch (e) { log('warn', 'read retry count: ' + e.message); }
|
|
1291
792
|
if (retryableFailure && retries < 3) {
|
|
1292
793
|
log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1293
794
|
updateWorkItemStatus(item.meta, 'pending', '');
|
|
@@ -1298,7 +799,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
1298
799
|
dp.completed = Array.isArray(dp.completed) ? dp.completed.filter(d => d.meta?.dispatchKey !== item.meta.dispatchKey) : [];
|
|
1299
800
|
return dp;
|
|
1300
801
|
});
|
|
1301
|
-
} catch {}
|
|
802
|
+
} catch (e) { log('warn', 'clear dispatch for retry: ' + e.message); }
|
|
1302
803
|
}
|
|
1303
804
|
// Increment retry counter on the source work item
|
|
1304
805
|
try {
|
|
@@ -1320,7 +821,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
1320
821
|
safeWrite(wiPath, items);
|
|
1321
822
|
}
|
|
1322
823
|
}
|
|
1323
|
-
} catch {}
|
|
824
|
+
} catch (e) { log('warn', 'increment retry counter: ' + e.message); }
|
|
1324
825
|
} else {
|
|
1325
826
|
const finalReason = !retryableFailure
|
|
1326
827
|
? `Non-retryable failure: ${reason || 'Unknown error'}`
|
|
@@ -1349,7 +850,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
1349
850
|
`These items cannot dispatch until \`${failedId}\` is fixed and reset to \`pending\`.\n`
|
|
1350
851
|
: `No downstream items are blocked.\n`)
|
|
1351
852
|
);
|
|
1352
|
-
} catch {}
|
|
853
|
+
} catch (e) { log('warn', 'write failure alert: ' + e.message); }
|
|
1353
854
|
}
|
|
1354
855
|
}
|
|
1355
856
|
}
|
|
@@ -1370,7 +871,7 @@ function areDependenciesMet(item, config) {
|
|
|
1370
871
|
try {
|
|
1371
872
|
const wi = safeJson(projectWorkItemsPath(p)) || [];
|
|
1372
873
|
allWorkItems = allWorkItems.concat(wi);
|
|
1373
|
-
} catch {}
|
|
874
|
+
} catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
|
|
1374
875
|
}
|
|
1375
876
|
// PRD item statuses that count as "done" for dep resolution
|
|
1376
877
|
const PRD_MET_STATUSES = new Set(['done', 'in-pr', 'implemented', 'complete']);
|
|
@@ -1383,7 +884,7 @@ function areDependenciesMet(item, config) {
|
|
|
1383
884
|
const plan = safeJson(path.join(PRD_DIR, sourcePlan));
|
|
1384
885
|
const prdItem = (plan?.missing_features || []).find(f => f.id === depId);
|
|
1385
886
|
if (prdItem && PRD_MET_STATUSES.has(prdItem.status)) continue; // PRD says done — treat as met
|
|
1386
|
-
} catch {}
|
|
887
|
+
} catch (e) { log('warn', 'check PRD dep status: ' + e.message); }
|
|
1387
888
|
log('warn', `Dependency ${depId} not found for ${item.id} (plan: ${sourcePlan}) — treating as unmet`);
|
|
1388
889
|
return false;
|
|
1389
890
|
}
|
|
@@ -1419,7 +920,7 @@ function writeInboxAlert(slug, content) {
|
|
|
1419
920
|
const existing = safeReadDir(INBOX_DIR).find(f => f.startsWith(`engine-alert-${slug}-${dateStamp()}`));
|
|
1420
921
|
if (existing) return;
|
|
1421
922
|
safeWrite(file, content);
|
|
1422
|
-
} catch {}
|
|
923
|
+
} catch (e) { log('warn', 'write inbox alert: ' + e.message); }
|
|
1423
924
|
}
|
|
1424
925
|
|
|
1425
926
|
// Reconciles work items against known PRs.
|
|
@@ -1526,7 +1027,7 @@ function checkSteering(config) {
|
|
|
1526
1027
|
if (!fs.existsSync(steerPath)) continue;
|
|
1527
1028
|
|
|
1528
1029
|
const message = safeRead(steerPath);
|
|
1529
|
-
try { fs.unlinkSync(steerPath); } catch {}
|
|
1030
|
+
try { fs.unlinkSync(steerPath); } catch { /* cleanup */ }
|
|
1530
1031
|
if (!message) continue;
|
|
1531
1032
|
|
|
1532
1033
|
const sessionId = info.sessionId;
|
|
@@ -1538,7 +1039,7 @@ function checkSteering(config) {
|
|
|
1538
1039
|
log('info', `Steering: killing ${info.agentId} (${id}) for session resume with human message`);
|
|
1539
1040
|
|
|
1540
1041
|
// Kill current process
|
|
1541
|
-
try { info.proc.kill('SIGTERM'); } catch {}
|
|
1042
|
+
try { info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
1542
1043
|
|
|
1543
1044
|
// Store steering context for re-spawn on close
|
|
1544
1045
|
info._steeringMessage = message;
|
|
@@ -1558,9 +1059,9 @@ function checkTimeouts(config) {
|
|
|
1558
1059
|
const elapsed = Date.now() - new Date(info.startedAt).getTime();
|
|
1559
1060
|
if (elapsed > itemTimeout) {
|
|
1560
1061
|
log('warn', `Agent ${info.agentId} (${id}) hit hard timeout after ${Math.round(elapsed / 1000)}s — killing`);
|
|
1561
|
-
try { info.proc.kill('SIGTERM'); } catch {}
|
|
1062
|
+
try { info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
1562
1063
|
setTimeout(() => {
|
|
1563
|
-
try { info.proc.kill('SIGKILL'); } catch {}
|
|
1064
|
+
try { info.proc.kill('SIGKILL'); } catch { /* process may be dead */ }
|
|
1564
1065
|
}, 5000);
|
|
1565
1066
|
}
|
|
1566
1067
|
}
|
|
@@ -1581,7 +1082,7 @@ function checkTimeouts(config) {
|
|
|
1581
1082
|
try {
|
|
1582
1083
|
const stat = fs.statSync(liveLogPath);
|
|
1583
1084
|
lastActivity = Math.max(lastActivity, stat.mtimeMs);
|
|
1584
|
-
} catch {}
|
|
1085
|
+
} catch { /* optional */ }
|
|
1585
1086
|
|
|
1586
1087
|
const silentMs = Date.now() - lastActivity;
|
|
1587
1088
|
const silentSec = Math.round(silentMs / 1000);
|
|
@@ -1605,7 +1106,7 @@ function checkTimeouts(config) {
|
|
|
1605
1106
|
const result = JSON.parse(resultLine);
|
|
1606
1107
|
safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${isSuccess ? 0 : 1}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${result.result || '(no text)'}\n`);
|
|
1607
1108
|
}
|
|
1608
|
-
} catch {}
|
|
1109
|
+
} catch (e) { log('warn', 'parse output result: ' + e.message); }
|
|
1609
1110
|
|
|
1610
1111
|
completeDispatch(item.id, isSuccess ? 'success' : 'error', 'Completed (detected from output)');
|
|
1611
1112
|
|
|
@@ -1613,12 +1114,12 @@ function checkTimeouts(config) {
|
|
|
1613
1114
|
runPostCompletionHooks(item, item.agent, isSuccess ? 0 : 1, liveLog, config);
|
|
1614
1115
|
|
|
1615
1116
|
if (hasProcess) {
|
|
1616
|
-
try { activeProcesses.get(item.id)?.proc.kill('SIGTERM'); } catch {}
|
|
1117
|
+
try { activeProcesses.get(item.id)?.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
1617
1118
|
activeProcesses.delete(item.id);
|
|
1618
1119
|
}
|
|
1619
1120
|
continue; // Skip orphan/hung detection — we handled it
|
|
1620
1121
|
}
|
|
1621
|
-
} catch {}
|
|
1122
|
+
} catch (e) { log('warn', 'output completion detection: ' + e.message); }
|
|
1622
1123
|
|
|
1623
1124
|
// Check if agent is in a blocking tool call (TaskOutput block:true, Bash with long timeout, etc.)
|
|
1624
1125
|
// These tools produce no stdout for extended periods — don't kill them prematurely
|
|
@@ -1652,13 +1153,13 @@ function checkTimeouts(config) {
|
|
|
1652
1153
|
isBlocking = true;
|
|
1653
1154
|
}
|
|
1654
1155
|
break; // only check the most recent tool_use
|
|
1655
|
-
} catch {}
|
|
1156
|
+
} catch { /* JSON parse — line may not be valid JSON */ }
|
|
1656
1157
|
}
|
|
1657
1158
|
if (isBlocking) {
|
|
1658
1159
|
log('info', `Agent ${item.agent} (${item.id}) is in a blocking tool call — extended timeout to ${Math.round(blockingTimeout / 1000)}s (silent for ${silentSec}s)`);
|
|
1659
1160
|
}
|
|
1660
1161
|
}
|
|
1661
|
-
} catch {}
|
|
1162
|
+
} catch (e) { log('warn', 'blocking tool detection: ' + e.message); }
|
|
1662
1163
|
}
|
|
1663
1164
|
|
|
1664
1165
|
const effectiveTimeout = isBlocking ? blockingTimeout : heartbeatTimeout;
|
|
@@ -1672,8 +1173,8 @@ function checkTimeouts(config) {
|
|
|
1672
1173
|
log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
|
|
1673
1174
|
const procInfo = activeProcesses.get(item.id);
|
|
1674
1175
|
if (procInfo) {
|
|
1675
|
-
try { procInfo.proc.kill('SIGTERM'); } catch {}
|
|
1676
|
-
setTimeout(() => { try { procInfo.proc.kill('SIGKILL'); } catch {} }, 5000);
|
|
1176
|
+
try { procInfo.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
1177
|
+
setTimeout(() => { try { procInfo.proc.kill('SIGKILL'); } catch { /* process may be dead */ } }, 5000);
|
|
1677
1178
|
activeProcesses.delete(item.id);
|
|
1678
1179
|
}
|
|
1679
1180
|
deadItems.push({ item, reason: `Hung — no output for ${silentSec}s` });
|
|
@@ -1752,11 +1253,11 @@ function runCleanup(config, verbose = false) {
|
|
|
1752
1253
|
fs.unlinkSync(fp);
|
|
1753
1254
|
cleaned.tempFiles++;
|
|
1754
1255
|
}
|
|
1755
|
-
} catch {}
|
|
1256
|
+
} catch { /* cleanup */ }
|
|
1756
1257
|
}
|
|
1757
1258
|
}
|
|
1758
1259
|
}
|
|
1759
|
-
} catch {}
|
|
1260
|
+
} catch (e) { log('warn', 'cleanup temp files: ' + e.message); }
|
|
1760
1261
|
|
|
1761
1262
|
// 2. Clean live-output.log for idle agents (not currently working)
|
|
1762
1263
|
for (const [agentId] of Object.entries(config.agents || {})) {
|
|
@@ -1770,7 +1271,7 @@ function runCleanup(config, verbose = false) {
|
|
|
1770
1271
|
fs.unlinkSync(livePath);
|
|
1771
1272
|
cleaned.liveOutputs++;
|
|
1772
1273
|
}
|
|
1773
|
-
} catch {}
|
|
1274
|
+
} catch { /* cleanup */ }
|
|
1774
1275
|
}
|
|
1775
1276
|
}
|
|
1776
1277
|
}
|
|
@@ -1835,7 +1336,7 @@ function runCleanup(config, verbose = false) {
|
|
|
1835
1336
|
if (ageMs > 7200000 && !isReferenced) { // 2 hours
|
|
1836
1337
|
shouldClean = true;
|
|
1837
1338
|
}
|
|
1838
|
-
} catch {}
|
|
1339
|
+
} catch { /* optional */ }
|
|
1839
1340
|
}
|
|
1840
1341
|
|
|
1841
1342
|
// Skip worktrees for active shared-branch plans (check both prd/ and plans/ for .json PRDs)
|
|
@@ -1859,7 +1360,7 @@ function runCleanup(config, verbose = false) {
|
|
|
1859
1360
|
}
|
|
1860
1361
|
if (isProtected) break;
|
|
1861
1362
|
}
|
|
1862
|
-
} catch {}
|
|
1363
|
+
} catch (e) { log('warn', 'check shared-branch protection: ' + e.message); }
|
|
1863
1364
|
}
|
|
1864
1365
|
|
|
1865
1366
|
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected });
|
|
@@ -1889,7 +1390,7 @@ function runCleanup(config, verbose = false) {
|
|
|
1889
1390
|
}
|
|
1890
1391
|
}
|
|
1891
1392
|
}
|
|
1892
|
-
} catch {}
|
|
1393
|
+
} catch (e) { log('warn', 'cleanup worktrees: ' + e.message); }
|
|
1893
1394
|
}
|
|
1894
1395
|
|
|
1895
1396
|
// 4. Kill zombie claude processes not tracked by the engine
|
|
@@ -1905,15 +1406,15 @@ function runCleanup(config, verbose = false) {
|
|
|
1905
1406
|
const activeIds = new Set((dispatch.active || []).map(d => d.id));
|
|
1906
1407
|
for (const [id, info] of activeProcesses.entries()) {
|
|
1907
1408
|
if (!activeIds.has(id)) {
|
|
1908
|
-
try { if (info.proc) info.proc.kill('SIGTERM'); } catch {}
|
|
1409
|
+
try { if (info.proc) info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
|
|
1909
1410
|
activeProcesses.delete(id);
|
|
1910
1411
|
cleaned.zombies++;
|
|
1911
1412
|
}
|
|
1912
1413
|
}
|
|
1913
|
-
} catch {}
|
|
1414
|
+
} catch (e) { log('warn', 'cleanup zombie processes: ' + e.message); }
|
|
1914
1415
|
|
|
1915
1416
|
// 5. Clean spawn-debug.log
|
|
1916
|
-
try { fs.unlinkSync(path.join(ENGINE_DIR, 'spawn-debug.log')); } catch {}
|
|
1417
|
+
try { fs.unlinkSync(path.join(ENGINE_DIR, 'spawn-debug.log')); } catch { /* cleanup */ }
|
|
1917
1418
|
|
|
1918
1419
|
// 6. Prune old output archive files (keep last 30 per agent)
|
|
1919
1420
|
for (const agentId of Object.keys(config.agents || {})) {
|
|
@@ -1925,9 +1426,9 @@ function runCleanup(config, verbose = false) {
|
|
|
1925
1426
|
.map(f => ({ name: f, mtime: fs.statSync(path.join(agentDir, f)).mtimeMs }))
|
|
1926
1427
|
.sort((a, b) => b.mtime - a.mtime);
|
|
1927
1428
|
for (const old of outputFiles.slice(30)) {
|
|
1928
|
-
try { fs.unlinkSync(path.join(agentDir, old.name)); cleaned.files++; } catch {}
|
|
1429
|
+
try { fs.unlinkSync(path.join(agentDir, old.name)); cleaned.files++; } catch { /* cleanup */ }
|
|
1929
1430
|
}
|
|
1930
|
-
} catch {}
|
|
1431
|
+
} catch (e) { log('warn', 'prune output archives: ' + e.message); }
|
|
1931
1432
|
}
|
|
1932
1433
|
|
|
1933
1434
|
// 7. Prune orphaned dispatch entries — items whose source work item no longer exists
|
|
@@ -1939,12 +1440,12 @@ function runCleanup(config, verbose = false) {
|
|
|
1939
1440
|
try {
|
|
1940
1441
|
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
1941
1442
|
central.forEach(w => allWiIds.add(w.id));
|
|
1942
|
-
} catch {}
|
|
1443
|
+
} catch (e) { log('warn', 'read central work items for orphan check: ' + e.message); }
|
|
1943
1444
|
for (const project of projects) {
|
|
1944
1445
|
try {
|
|
1945
1446
|
const projItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
1946
1447
|
projItems.forEach(w => allWiIds.add(w.id));
|
|
1947
|
-
} catch {}
|
|
1448
|
+
} catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
|
|
1948
1449
|
}
|
|
1949
1450
|
|
|
1950
1451
|
let changed = false;
|
|
@@ -1974,7 +1475,7 @@ function runCleanup(config, verbose = false) {
|
|
|
1974
1475
|
}
|
|
1975
1476
|
});
|
|
1976
1477
|
}
|
|
1977
|
-
} catch {}
|
|
1478
|
+
} catch (e) { log('warn', 'prune orphaned dispatches: ' + e.message); }
|
|
1978
1479
|
|
|
1979
1480
|
if (cleaned.tempFiles + cleaned.liveOutputs + cleaned.worktrees + cleaned.zombies + (cleaned.files || 0) + cleaned.orphanedDispatches > 0) {
|
|
1980
1481
|
log('info', `Cleanup: ${cleaned.tempFiles} temp, ${cleaned.liveOutputs} live outputs, ${cleaned.worktrees} worktrees, ${cleaned.zombies} zombies, ${cleaned.files || 0} archives, ${cleaned.orphanedDispatches} orphaned dispatches`);
|
|
@@ -1993,10 +1494,10 @@ function runCleanup(config, verbose = false) {
|
|
|
1993
1494
|
if (!cleaned.sweptKb) cleaned.sweptKb = 0;
|
|
1994
1495
|
cleaned.sweptKb++;
|
|
1995
1496
|
}
|
|
1996
|
-
} catch {}
|
|
1497
|
+
} catch { /* cleanup */ }
|
|
1997
1498
|
}
|
|
1998
1499
|
}
|
|
1999
|
-
} catch {}
|
|
1500
|
+
} catch (e) { log('warn', 'cleanup swept KB files: ' + e.message); }
|
|
2000
1501
|
|
|
2001
1502
|
// 9. KB watchdog — restore deleted KB files from git if count dropped vs checkpoint
|
|
2002
1503
|
try {
|
|
@@ -2024,7 +1525,7 @@ function runCleanup(config, verbose = false) {
|
|
|
2024
1525
|
}
|
|
2025
1526
|
}
|
|
2026
1527
|
}
|
|
2027
|
-
} catch {}
|
|
1528
|
+
} catch (e) { log('warn', 'KB watchdog check: ' + e.message); }
|
|
2028
1529
|
|
|
2029
1530
|
// 6. Migrate legacy work-item statuses to canonical values
|
|
2030
1531
|
// in-pr, implemented, complete → done (one-time correction per item)
|
|
@@ -2044,7 +1545,7 @@ function runCleanup(config, verbose = false) {
|
|
|
2044
1545
|
safeWrite(wiPath, items);
|
|
2045
1546
|
log('info', `Migrated ${migrated} legacy status(es) → done in ${project.name} work items`);
|
|
2046
1547
|
}
|
|
2047
|
-
} catch {}
|
|
1548
|
+
} catch (e) { log('warn', 'migrate legacy statuses: ' + e.message); }
|
|
2048
1549
|
}
|
|
2049
1550
|
// Central work items
|
|
2050
1551
|
try {
|
|
@@ -2061,7 +1562,7 @@ function runCleanup(config, verbose = false) {
|
|
|
2061
1562
|
safeWrite(centralPath, centralItems);
|
|
2062
1563
|
log('info', `Migrated ${migrated} legacy status(es) → done in central work items`);
|
|
2063
1564
|
}
|
|
2064
|
-
} catch {}
|
|
1565
|
+
} catch (e) { log('warn', 'migrate central legacy statuses: ' + e.message); }
|
|
2065
1566
|
// PRD items (missing_features[].status)
|
|
2066
1567
|
try {
|
|
2067
1568
|
const prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
|
|
@@ -2081,101 +1582,16 @@ function runCleanup(config, verbose = false) {
|
|
|
2081
1582
|
log('info', `Migrated ${migrated} legacy PRD item status(es) → done in ${pf}`);
|
|
2082
1583
|
}
|
|
2083
1584
|
}
|
|
2084
|
-
} catch {}
|
|
1585
|
+
} catch (e) { log('warn', 'migrate PRD legacy statuses: ' + e.message); }
|
|
2085
1586
|
|
|
2086
1587
|
return cleaned;
|
|
2087
1588
|
}
|
|
2088
1589
|
|
|
2089
|
-
// ───
|
|
2090
|
-
|
|
2091
|
-
const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
2092
|
-
const dispatchCooldowns = new Map(); // key → { timestamp, failures }
|
|
2093
|
-
|
|
2094
|
-
function loadCooldowns() {
|
|
2095
|
-
const saved = safeJson(COOLDOWN_PATH);
|
|
2096
|
-
if (!saved) return;
|
|
2097
|
-
const now = Date.now();
|
|
2098
|
-
for (const [k, v] of Object.entries(saved)) {
|
|
2099
|
-
// Prune entries older than 24 hours
|
|
2100
|
-
if (now - v.timestamp < 24 * 60 * 60 * 1000) {
|
|
2101
|
-
dispatchCooldowns.set(k, v);
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
log('info', `Loaded ${dispatchCooldowns.size} cooldowns from disk`);
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
let _cooldownWriteTimer = null;
|
|
2108
|
-
function saveCooldowns() {
|
|
2109
|
-
// Debounce: reset timer on each call so latest state is always written
|
|
2110
|
-
if (_cooldownWriteTimer) clearTimeout(_cooldownWriteTimer);
|
|
2111
|
-
_cooldownWriteTimer = setTimeout(() => {
|
|
2112
|
-
_cooldownWriteTimer = null;
|
|
2113
|
-
// Prune expired entries (>24h) before saving
|
|
2114
|
-
const now = Date.now();
|
|
2115
|
-
for (const [k, v] of dispatchCooldowns) {
|
|
2116
|
-
if (now - v.timestamp > 24 * 60 * 60 * 1000) dispatchCooldowns.delete(k);
|
|
2117
|
-
}
|
|
2118
|
-
const obj = Object.fromEntries(dispatchCooldowns);
|
|
2119
|
-
safeWrite(COOLDOWN_PATH, obj);
|
|
2120
|
-
}, 1000); // debounce — write at most once per second
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
function isOnCooldown(key, cooldownMs) {
|
|
2124
|
-
const entry = dispatchCooldowns.get(key);
|
|
2125
|
-
if (!entry) return false;
|
|
2126
|
-
const backoff = Math.min(Math.pow(2, entry.failures || 0), 8);
|
|
2127
|
-
return (Date.now() - entry.timestamp) < (cooldownMs * backoff);
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
function setCooldown(key) {
|
|
2131
|
-
const existing = dispatchCooldowns.get(key);
|
|
2132
|
-
dispatchCooldowns.set(key, { timestamp: Date.now(), failures: existing?.failures || 0 });
|
|
2133
|
-
saveCooldowns();
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
function setCooldownWithContext(key, context) {
|
|
2137
|
-
const existing = dispatchCooldowns.get(key);
|
|
2138
|
-
const pendingContexts = existing?.pendingContexts || [];
|
|
2139
|
-
if (context) pendingContexts.push(context);
|
|
2140
|
-
dispatchCooldowns.set(key, {
|
|
2141
|
-
timestamp: Date.now(),
|
|
2142
|
-
failures: existing?.failures || 0,
|
|
2143
|
-
pendingContexts
|
|
2144
|
-
});
|
|
2145
|
-
saveCooldowns();
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
function getCoalescedContexts(key) {
|
|
2149
|
-
const entry = dispatchCooldowns.get(key);
|
|
2150
|
-
const contexts = entry?.pendingContexts || [];
|
|
2151
|
-
if (contexts.length > 0 && entry) {
|
|
2152
|
-
entry.pendingContexts = []; // Clear after retrieval
|
|
2153
|
-
}
|
|
2154
|
-
return contexts;
|
|
2155
|
-
}
|
|
1590
|
+
// ─── Cooldowns (extracted to engine/cooldown.js) ─────────────────────────────
|
|
2156
1591
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
dispatchCooldowns.set(key, { timestamp: Date.now(), failures });
|
|
2161
|
-
if (failures >= 3) {
|
|
2162
|
-
log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures), 8)}x`);
|
|
2163
|
-
}
|
|
2164
|
-
saveCooldowns();
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
function isAlreadyDispatched(key) {
|
|
2168
|
-
const dispatch = getDispatch();
|
|
2169
|
-
// Check pending and active
|
|
2170
|
-
const inFlight = [...dispatch.pending, ...(dispatch.active || [])];
|
|
2171
|
-
if (inFlight.some(d => d.meta?.dispatchKey === key)) return true;
|
|
2172
|
-
// Also check recently completed (last hour) to prevent re-dispatch
|
|
2173
|
-
const oneHourAgo = Date.now() - 3600000;
|
|
2174
|
-
const recentCompleted = (dispatch.completed || []).filter(d =>
|
|
2175
|
-
d.completed_at && new Date(d.completed_at).getTime() > oneHourAgo
|
|
2176
|
-
);
|
|
2177
|
-
return recentCompleted.some(d => d.meta?.dispatchKey === key);
|
|
2178
|
-
}
|
|
1592
|
+
const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
1593
|
+
isOnCooldown, setCooldown, setCooldownWithContext, getCoalescedContexts,
|
|
1594
|
+
setCooldownFailure, isAlreadyDispatched } = require('./engine/cooldown');
|
|
2179
1595
|
|
|
2180
1596
|
|
|
2181
1597
|
|
|
@@ -2204,7 +1620,7 @@ function autoCleanPrdWorkItems(prdFile, config) {
|
|
|
2204
1620
|
return true;
|
|
2205
1621
|
});
|
|
2206
1622
|
if (filtered.length < items.length) safeWrite(wiPath, filtered);
|
|
2207
|
-
} catch {}
|
|
1623
|
+
} catch (e) { log('warn', 'auto-clean PRD work items: ' + e.message); }
|
|
2208
1624
|
}
|
|
2209
1625
|
if (deletedIds.length > 0) {
|
|
2210
1626
|
const deletedSet = new Set(deletedIds);
|
|
@@ -2214,7 +1630,7 @@ function autoCleanPrdWorkItems(prdFile, config) {
|
|
|
2214
1630
|
}
|
|
2215
1631
|
|
|
2216
1632
|
function materializePlansAsWorkItems(config) {
|
|
2217
|
-
if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch {} }
|
|
1633
|
+
if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
|
|
2218
1634
|
|
|
2219
1635
|
// Enforce: PRDs must be .json — auto-rename .md files that contain valid PRD JSON
|
|
2220
1636
|
// Check both prd/ and plans/ (agents may still write JSON to plans/)
|
|
@@ -2231,12 +1647,12 @@ function materializePlansAsWorkItems(config) {
|
|
|
2231
1647
|
if (parsed.missing_features) {
|
|
2232
1648
|
const jsonName = mf.replace(/\.md$/, '.json');
|
|
2233
1649
|
safeWrite(path.join(PRD_DIR, jsonName), parsed);
|
|
2234
|
-
try { fs.unlinkSync(path.join(checkDir, mf)); } catch {}
|
|
1650
|
+
try { fs.unlinkSync(path.join(checkDir, mf)); } catch { /* cleanup */ }
|
|
2235
1651
|
log('info', `Plan enforcement: moved ${mf} → prd/${jsonName} (PRDs must be .json in prd/)`);
|
|
2236
1652
|
}
|
|
2237
1653
|
} catch {} // Not JSON — it's a proper plan .md, leave it
|
|
2238
1654
|
}
|
|
2239
|
-
} catch {}
|
|
1655
|
+
} catch (e) { log('warn', 'scan .md files for PRD enforcement: ' + e.message); }
|
|
2240
1656
|
// Also migrate any .json PRD files from plans/ to prd/
|
|
2241
1657
|
if (checkDir === PLANS_DIR) {
|
|
2242
1658
|
try {
|
|
@@ -2246,12 +1662,12 @@ function materializePlansAsWorkItems(config) {
|
|
|
2246
1662
|
const parsed = safeJson(path.join(PLANS_DIR, jf));
|
|
2247
1663
|
if (parsed?.missing_features) {
|
|
2248
1664
|
safeWrite(path.join(PRD_DIR, jf), parsed);
|
|
2249
|
-
try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch {}
|
|
1665
|
+
try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch { /* cleanup */ }
|
|
2250
1666
|
log('info', `Auto-migrated PRD ${jf} from plans/ to prd/`);
|
|
2251
1667
|
}
|
|
2252
|
-
} catch {}
|
|
1668
|
+
} catch (e) { log('warn', 'migrate PRD from plans: ' + e.message); }
|
|
2253
1669
|
}
|
|
2254
|
-
} catch {}
|
|
1670
|
+
} catch (e) { log('warn', 'scan JSON in plans dir: ' + e.message); }
|
|
2255
1671
|
}
|
|
2256
1672
|
}
|
|
2257
1673
|
|
|
@@ -2303,7 +1719,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2303
1719
|
: '';
|
|
2304
1720
|
|
|
2305
1721
|
// Delete old PRD — agent will write replacement at same path
|
|
2306
|
-
try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch {}
|
|
1722
|
+
try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch { /* cleanup */ }
|
|
2307
1723
|
|
|
2308
1724
|
// Queue plan-to-prd regeneration
|
|
2309
1725
|
const planContent = safeRead(path.join(PLANS_DIR, plan.source_plan));
|
|
@@ -2341,7 +1757,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2341
1757
|
|
|
2342
1758
|
safeWrite(path.join(PRD_DIR, file), plan);
|
|
2343
1759
|
}
|
|
2344
|
-
} catch {}
|
|
1760
|
+
} catch (e) { log('warn', 'plan staleness check: ' + e.message); }
|
|
2345
1761
|
}
|
|
2346
1762
|
|
|
2347
1763
|
// Human approval gate: plans start as 'awaiting-approval' and must be approved before work begins
|
|
@@ -2507,50 +1923,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2507
1923
|
}
|
|
2508
1924
|
}
|
|
2509
1925
|
|
|
2510
|
-
//
|
|
2511
|
-
|
|
2512
|
-
function buildBaseVars(agentId, config, project) {
|
|
2513
|
-
return {
|
|
2514
|
-
agent_id: agentId,
|
|
2515
|
-
agent_name: config.agents[agentId]?.name || agentId,
|
|
2516
|
-
agent_role: config.agents[agentId]?.role || 'Agent',
|
|
2517
|
-
team_root: MINIONS_DIR,
|
|
2518
|
-
repo_id: project?.repositoryId || '',
|
|
2519
|
-
project_name: project?.name || 'Unknown Project',
|
|
2520
|
-
ado_org: project?.adoOrg || 'Unknown',
|
|
2521
|
-
ado_project: project?.adoProject || 'Unknown',
|
|
2522
|
-
repo_name: project?.repoName || 'Unknown',
|
|
2523
|
-
main_branch: project?.mainBranch || 'main',
|
|
2524
|
-
date: dateStamp(),
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
function selectPlaybook(workType, item) {
|
|
2529
|
-
if (item?.branchStrategy === 'shared-branch' && (workType === 'implement' || workType === 'implement:large')) {
|
|
2530
|
-
return 'implement-shared';
|
|
2531
|
-
}
|
|
2532
|
-
if (workType === 'review' && !item?._pr && !item?.pr_id) {
|
|
2533
|
-
return 'work-item';
|
|
2534
|
-
}
|
|
2535
|
-
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose'];
|
|
2536
|
-
return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabel, meta) {
|
|
2540
|
-
const vars = { ...buildBaseVars(agentId, config, project), ...extraVars };
|
|
2541
|
-
const playbookName = type === 'test' ? 'build-and-test' : (type === 'review' ? 'review' : 'fix');
|
|
2542
|
-
const prompt = renderPlaybook(playbookName, vars);
|
|
2543
|
-
if (!prompt) return null;
|
|
2544
|
-
return {
|
|
2545
|
-
type,
|
|
2546
|
-
agent: agentId,
|
|
2547
|
-
agentName: config.agents[agentId]?.name,
|
|
2548
|
-
agentRole: config.agents[agentId]?.role,
|
|
2549
|
-
task: `[${project?.name || 'project'}] ${taskLabel}`,
|
|
2550
|
-
prompt,
|
|
2551
|
-
meta,
|
|
2552
|
-
};
|
|
2553
|
-
}
|
|
1926
|
+
// buildBaseVars, selectPlaybook, buildPrDispatch extracted to engine/playbook.js
|
|
2554
1927
|
|
|
2555
1928
|
function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
2556
1929
|
if (!prId) return;
|
|
@@ -2561,7 +1934,7 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
2561
1934
|
if (!target?.humanFeedback?.pendingFix) return;
|
|
2562
1935
|
target.humanFeedback.pendingFix = false;
|
|
2563
1936
|
safeWrite(prsPath, prs);
|
|
2564
|
-
} catch {}
|
|
1937
|
+
} catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
|
|
2565
1938
|
}
|
|
2566
1939
|
|
|
2567
1940
|
/**
|
|
@@ -2681,7 +2054,7 @@ function discoverFromPrs(config, project) {
|
|
|
2681
2054
|
target._buildFailNotified = true;
|
|
2682
2055
|
safeWrite(prPath, prs);
|
|
2683
2056
|
}
|
|
2684
|
-
} catch {}
|
|
2057
|
+
} catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
|
|
2685
2058
|
}
|
|
2686
2059
|
}
|
|
2687
2060
|
|
|
@@ -2747,7 +2120,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2747
2120
|
return dp.completed.length !== before ? dp : undefined;
|
|
2748
2121
|
});
|
|
2749
2122
|
dispatchCooldowns.delete(key);
|
|
2750
|
-
} catch {}
|
|
2123
|
+
} catch (e) { log('warn', 'self-heal dispatch state: ' + e.message); }
|
|
2751
2124
|
// Cooldown bypass for resumed items — clear in-memory cooldown so they dispatch immediately
|
|
2752
2125
|
if (item._resumedAt) {
|
|
2753
2126
|
dispatchCooldowns.delete(key);
|
|
@@ -2809,7 +2182,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2809
2182
|
commit_message: item.commitMessage || `feat: ${item.title || item.id}`,
|
|
2810
2183
|
notes_content: '',
|
|
2811
2184
|
};
|
|
2812
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2185
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
2813
2186
|
|
|
2814
2187
|
// Inject references and acceptance criteria
|
|
2815
2188
|
const refs = (item.references || []).filter(r => r && r.url).map(r =>
|
|
@@ -2824,7 +2197,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2824
2197
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
2825
2198
|
vars.task_id = item.id;
|
|
2826
2199
|
vars.notes_content = '';
|
|
2827
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2200
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
2828
2201
|
}
|
|
2829
2202
|
|
|
2830
2203
|
// Resolve implicit context references (e.g., "ripley's plan", "the latest plan")
|
|
@@ -2954,7 +2327,7 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
2954
2327
|
recentSpecs.push({ file: line.trim(), ...currentCommit });
|
|
2955
2328
|
}
|
|
2956
2329
|
}
|
|
2957
|
-
} catch {}
|
|
2330
|
+
} catch (e) { log('warn', 'git: ' + e.message); }
|
|
2958
2331
|
}
|
|
2959
2332
|
|
|
2960
2333
|
if (recentSpecs.length === 0) return;
|
|
@@ -3118,7 +2491,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3118
2491
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3119
2492
|
vars.task_id = item.id;
|
|
3120
2493
|
vars.notes_content = '';
|
|
3121
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2494
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
3122
2495
|
}
|
|
3123
2496
|
|
|
3124
2497
|
const resolvedCtx = resolveTaskContext(item, config);
|
|
@@ -3179,7 +2552,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3179
2552
|
project_path: firstProject?.localPath || '',
|
|
3180
2553
|
notes_content: '',
|
|
3181
2554
|
};
|
|
3182
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2555
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
3183
2556
|
|
|
3184
2557
|
// Inject references and acceptance criteria
|
|
3185
2558
|
const normRefs = (item.references || []).filter(r => r && r.url).map(r =>
|
|
@@ -3199,7 +2572,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3199
2572
|
vars.plan_file = planFileName;
|
|
3200
2573
|
vars.task_description = item.title;
|
|
3201
2574
|
vars.notes_content = '';
|
|
3202
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2575
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
3203
2576
|
// Track expected plan filename in meta for chainPlanToPrd
|
|
3204
2577
|
item._planFileName = planFileName;
|
|
3205
2578
|
}
|
|
@@ -3228,7 +2601,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3228
2601
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3229
2602
|
vars.task_id = item.id;
|
|
3230
2603
|
vars.notes_content = '';
|
|
3231
|
-
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
2604
|
+
try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
|
|
3232
2605
|
}
|
|
3233
2606
|
|
|
3234
2607
|
// Resolve implicit context references
|
|
@@ -3321,7 +2694,7 @@ function discoverWork(config) {
|
|
|
3321
2694
|
}
|
|
3322
2695
|
if (added > 0) safeWrite(centralPath, items);
|
|
3323
2696
|
}
|
|
3324
|
-
} catch {}
|
|
2697
|
+
} catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
|
|
3325
2698
|
|
|
3326
2699
|
// Gate reviews and fixes: do not dispatch until all implement items are complete
|
|
3327
2700
|
const hasIncompleteImplements = projects.some(project => {
|
|
@@ -3381,7 +2754,7 @@ async function tickInner() {
|
|
|
3381
2754
|
}
|
|
3382
2755
|
|
|
3383
2756
|
// Write heartbeat so dashboard can detect stale engine
|
|
3384
|
-
try { safeWrite(CONTROL_PATH, { ...control, heartbeat: Date.now() }); } catch {}
|
|
2757
|
+
try { safeWrite(CONTROL_PATH, { ...control, heartbeat: Date.now() }); } catch (e) { log('warn', 'write heartbeat: ' + e.message); }
|
|
3385
2758
|
|
|
3386
2759
|
const config = getConfig();
|
|
3387
2760
|
tickCount++;
|
|
@@ -3480,7 +2853,7 @@ async function tickInner() {
|
|
|
3480
2853
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
3481
2854
|
if (dp.completed.length !== before) return dp;
|
|
3482
2855
|
});
|
|
3483
|
-
} catch {}
|
|
2856
|
+
} catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
|
|
3484
2857
|
|
|
3485
2858
|
// Clear cooldown so item isn't blocked by exponential backoff
|
|
3486
2859
|
try {
|
|
@@ -3489,7 +2862,7 @@ async function tickInner() {
|
|
|
3489
2862
|
dispatchCooldowns.delete(key);
|
|
3490
2863
|
saveCooldowns();
|
|
3491
2864
|
}
|
|
3492
|
-
} catch {}
|
|
2865
|
+
} catch (e) { log('warn', 'stall recovery clear cooldown: ' + e.message); }
|
|
3493
2866
|
}
|
|
3494
2867
|
}
|
|
3495
2868
|
|
|
@@ -3513,14 +2886,14 @@ async function tickInner() {
|
|
|
3513
2886
|
mutateDispatch((dp) => {
|
|
3514
2887
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
3515
2888
|
});
|
|
3516
|
-
} catch {}
|
|
2889
|
+
} catch (e) { log('warn', 'stall recovery clear dependent dispatch: ' + e.message); }
|
|
3517
2890
|
}
|
|
3518
2891
|
}
|
|
3519
2892
|
}
|
|
3520
2893
|
}
|
|
3521
2894
|
|
|
3522
2895
|
if (changed) safeWrite(wiPath, items);
|
|
3523
|
-
} catch {}
|
|
2896
|
+
} catch (e) { log('warn', 'stall recovery process project: ' + e.message); }
|
|
3524
2897
|
}
|
|
3525
2898
|
}
|
|
3526
2899
|
} catch (err) { log('warn', `Stall detection error: ${err?.message || err}`); }
|