@yemi33/minions 0.1.1971 → 0.1.1972
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/dashboard/js/render-work-items.js +2 -2
- package/docs/managed-spawn.md +1 -1
- package/engine/cleanup.js +39 -0
- package/engine/consolidation.js +306 -26
- package/engine/dispatch.js +58 -2
- package/engine/lifecycle.js +24 -2
- package/engine/playbook.js +2 -1
- package/engine/shared.js +14 -1
- package/engine.js +18 -6
- package/package.json +1 -1
- package/playbooks/setup.md +113 -0
- package/prompts/cc-system.md +3 -3
- package/routing.md +1 -0
|
@@ -128,7 +128,7 @@ function renderWorkItems(items) {
|
|
|
128
128
|
function editWorkItem(id, source) {
|
|
129
129
|
const item = allWorkItems.find(i => i.id === id);
|
|
130
130
|
if (!item) return;
|
|
131
|
-
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs'];
|
|
131
|
+
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs', 'setup'];
|
|
132
132
|
const priorities = ['critical', 'high', 'medium', 'low'];
|
|
133
133
|
const agentOpts = (cmdAgents || []).map(a => '<option value="' + escapeHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escapeHtml(a.name) + '</option>').join('');
|
|
134
134
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
@@ -352,7 +352,7 @@ async function submitFeedback(id, source) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
function openCreateWorkItemModal() {
|
|
355
|
-
const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan', 'verify', 'decompose', 'meeting', 'docs'].map(t =>
|
|
355
|
+
const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan', 'verify', 'decompose', 'meeting', 'docs', 'setup'].map(t =>
|
|
356
356
|
'<option value="' + t + '"' + (t === 'implement' ? ' selected' : '') + '>' + t + '</option>'
|
|
357
357
|
).join('');
|
|
358
358
|
const priOpts = ['critical', 'high', 'medium', 'low'].map(p =>
|
package/docs/managed-spawn.md
CHANGED
|
@@ -202,7 +202,7 @@ All knobs live under `engine.managedSpawn` in `engine/shared.js:1500` (`ENGINE_D
|
|
|
202
202
|
| `enabled` | `true` | Global kill switch. `false` makes the engine ignore all sidecars + skip the sweep. |
|
|
203
203
|
| `maxSpecsPerFile` | `5` | Per-agent cap. |
|
|
204
204
|
| `maxTtlMinutes` | `1440` | Hard cap (24h). |
|
|
205
|
-
| `defaultTtlMinutes` | `
|
|
205
|
+
| `defaultTtlMinutes` | `720` | Fallback when `ttl_minutes` omitted (12h). |
|
|
206
206
|
| `sweepEvery` | `30` | Ticks between sweeps. Default tick = 60s ⇒ ~30 min. |
|
|
207
207
|
| `defaultHealthIntervalSec` | `1` | Healthcheck cadence pre-first-healthy. |
|
|
208
208
|
| `healthBackoffSec` | `30` | Healthcheck cadence post-first-healthy. |
|
package/engine/cleanup.js
CHANGED
|
@@ -471,6 +471,25 @@ async function runCleanup(config, verbose = false) {
|
|
|
471
471
|
} catch (e) { log('warn', `worktree-pool: cleanup lookup failed: ${e.message}`); }
|
|
472
472
|
const _normalizePoolPath = worktreePool()._normalizePath;
|
|
473
473
|
|
|
474
|
+
// W-mpbinmrh001907e9 — managed-spawn cwd anchor protection. After a
|
|
475
|
+
// managed_spawn dispatch succeeds, engine-owned services run with
|
|
476
|
+
// `cwd` inside the dispatch's worktree; the agent is gone so no
|
|
477
|
+
// active dispatch references the branch and the >2h age sweep would
|
|
478
|
+
// reap the worktree from under the live services. Build a list of
|
|
479
|
+
// normalized cwds from engine/managed-processes.json and protect any
|
|
480
|
+
// worktree dir that contains one. Cwd is optional per the schema, so
|
|
481
|
+
// entries without cwd contribute nothing.
|
|
482
|
+
const _managedSpawnCwds = [];
|
|
483
|
+
try {
|
|
484
|
+
const _ms = require('./managed-spawn');
|
|
485
|
+
for (const rec of _ms.listManagedSpecs()) {
|
|
486
|
+
if (rec && typeof rec.cwd === 'string' && rec.cwd.length > 0) {
|
|
487
|
+
try { _managedSpawnCwds.push(path.resolve(rec.cwd)); }
|
|
488
|
+
catch (_e) { /* malformed cwd — skip */ }
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} catch (e) { log('warn', `managed-spawn cwd anchor lookup failed: ${e.message}`); }
|
|
492
|
+
|
|
474
493
|
// Probe `git branch --show-current` for every worktree in chunks of 5.
|
|
475
494
|
// Sequential probing was the dominant cost in the cleanup phase
|
|
476
495
|
// (5–15s tick stall every 10 ticks at 50+ worktrees), but unbounded
|
|
@@ -538,6 +557,26 @@ async function runCleanup(config, verbose = false) {
|
|
|
538
557
|
if (verbose) console.log(` Skipping worktree ${dir}: pool-borrowed by active dispatch`);
|
|
539
558
|
}
|
|
540
559
|
|
|
560
|
+
// W-mpbinmrh001907e9 — managed-spawn cwd anchor protection.
|
|
561
|
+
// Worktrees backing live managed-spawn cwds must outlive the
|
|
562
|
+
// originating dispatch; the engine-owned services run inside them.
|
|
563
|
+
// Overrides merged-branch and age sweeps because the cwd record is
|
|
564
|
+
// the authoritative signal that something live still uses the dir.
|
|
565
|
+
if (_managedSpawnCwds.length > 0) {
|
|
566
|
+
const _wtPathNorm = path.resolve(wtPath);
|
|
567
|
+
const _wtPathPrefix = _wtPathNorm + path.sep;
|
|
568
|
+
for (const cwd of _managedSpawnCwds) {
|
|
569
|
+
if (cwd === _wtPathNorm || cwd.startsWith(_wtPathPrefix)) {
|
|
570
|
+
isProtected = true;
|
|
571
|
+
if (shouldClean) {
|
|
572
|
+
shouldClean = false;
|
|
573
|
+
if (verbose) console.log(` Skipping worktree ${dir}: managed-spawn cwd anchor`);
|
|
574
|
+
}
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
541
580
|
// Also clean worktrees older than 2 hours with no active dispatch referencing them
|
|
542
581
|
let mtime = Date.now();
|
|
543
582
|
if (!shouldClean) {
|
package/engine/consolidation.js
CHANGED
|
@@ -25,6 +25,21 @@ const AGENT_MEMORY_BUDGET_BYTES = 25000;
|
|
|
25
25
|
// excludes temp-* IDs which we filter separately.
|
|
26
26
|
const AGENT_ID_PATTERN = /^[a-z][a-z0-9-]{0,40}$/;
|
|
27
27
|
|
|
28
|
+
// W-mpbi7qus0011bf77 — per-agent memory reconciliation tunables.
|
|
29
|
+
// Skip reconcile when existing memory is small; one mistaken fact in a tiny
|
|
30
|
+
// file is just noise, not a "stale facts coexisting with corrections" risk.
|
|
31
|
+
const AGENT_MEMORY_RECONCILE_MIN_EXISTING_BYTES = 1024;
|
|
32
|
+
// Cap the existing-memory payload sent to the LLM (use the most recent tail
|
|
33
|
+
// since contradictions usually concern recent assertions).
|
|
34
|
+
const AGENT_MEMORY_RECONCILE_LLM_CAP_BYTES = 10000;
|
|
35
|
+
// Hard safety guard: if the reconciled memory shrinks below this ratio of the
|
|
36
|
+
// pre-reconcile size, the LLM probably went rogue — abort the reconcile and
|
|
37
|
+
// fall back to a plain append.
|
|
38
|
+
const AGENT_MEMORY_RECONCILE_MIN_RETAIN_RATIO = 0.30;
|
|
39
|
+
// Contradiction / correction / failure signal heuristic. Conservative — we'd
|
|
40
|
+
// rather miss a stale fact than reconcile every benign "I learned X" note.
|
|
41
|
+
const AGENT_MEMORY_RECONCILE_SIGNAL_RE = /\b(invalid|rejected|rejection|incorrect|wrong|does not exist|never existed|stale|superseded?|_failureClass|invalid_managed_spawn)\b|(^|\n)\s*(\*\*)?reason:/i;
|
|
42
|
+
|
|
28
43
|
/**
|
|
29
44
|
* Extract the authoring agent for an inbox item.
|
|
30
45
|
* Prefers YAML frontmatter `agent:` field; falls back to filename prefix
|
|
@@ -84,31 +99,7 @@ function appendToAgentMemory(item, knownAgents) {
|
|
|
84
99
|
try {
|
|
85
100
|
shared.withFileLock(memPath + '.lock', () => {
|
|
86
101
|
const existing = (fs.existsSync(memPath) ? safeRead(memPath) : '') || '';
|
|
87
|
-
|
|
88
|
-
if (Buffer.byteLength(next, 'utf8') > AGENT_MEMORY_BUDGET_BYTES) {
|
|
89
|
-
// Find the last section boundary that keeps us under budget.
|
|
90
|
-
const limit = AGENT_MEMORY_BUDGET_BYTES;
|
|
91
|
-
// Keep the header (everything before the first '\n---\n\n### ' boundary)
|
|
92
|
-
// and as many recent sections as fit.
|
|
93
|
-
const firstBoundary = next.indexOf('\n---\n\n### ');
|
|
94
|
-
if (firstBoundary > 0) {
|
|
95
|
-
const header = next.slice(0, firstBoundary);
|
|
96
|
-
const rest = next.slice(firstBoundary);
|
|
97
|
-
// Drop oldest sections until we're under budget.
|
|
98
|
-
const sections = rest.split('\n---\n\n### ').filter(Boolean);
|
|
99
|
-
let trimmed = sections;
|
|
100
|
-
while (trimmed.length > 1 &&
|
|
101
|
-
Buffer.byteLength(header + '\n---\n\n### ' + trimmed.join('\n---\n\n### '), 'utf8') > limit) {
|
|
102
|
-
trimmed = trimmed.slice(1);
|
|
103
|
-
}
|
|
104
|
-
next = header + '\n---\n\n### ' + trimmed.join('\n---\n\n### ');
|
|
105
|
-
if (!next.endsWith('\n')) next += '\n';
|
|
106
|
-
} else {
|
|
107
|
-
// No boundaries — just truncate from the end (rare).
|
|
108
|
-
next = next.slice(-limit);
|
|
109
|
-
}
|
|
110
|
-
log('info', `Pruned knowledge/agents/${agent}.md to stay under ${limit} bytes`);
|
|
111
|
-
}
|
|
102
|
+
const next = pruneAgentMemoryToBudget(existing + entry, agent);
|
|
112
103
|
safeWrite(memPath, next);
|
|
113
104
|
});
|
|
114
105
|
return true;
|
|
@@ -118,6 +109,273 @@ function appendToAgentMemory(item, knownAgents) {
|
|
|
118
109
|
}
|
|
119
110
|
}
|
|
120
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Prune an agent memory file's content to AGENT_MEMORY_BUDGET_BYTES.
|
|
114
|
+
* Drops the oldest sections (after the header) until the result fits.
|
|
115
|
+
* Returns the (possibly identical) content.
|
|
116
|
+
*/
|
|
117
|
+
function pruneAgentMemoryToBudget(content, agent) {
|
|
118
|
+
if (Buffer.byteLength(content, 'utf8') <= AGENT_MEMORY_BUDGET_BYTES) return content;
|
|
119
|
+
const limit = AGENT_MEMORY_BUDGET_BYTES;
|
|
120
|
+
let next = content;
|
|
121
|
+
// Keep the header (everything before the first '\n---\n\n### ' boundary)
|
|
122
|
+
// and as many recent sections as fit.
|
|
123
|
+
const firstBoundary = next.indexOf('\n---\n\n### ');
|
|
124
|
+
if (firstBoundary > 0) {
|
|
125
|
+
const header = next.slice(0, firstBoundary);
|
|
126
|
+
const rest = next.slice(firstBoundary);
|
|
127
|
+
const sections = rest.split('\n---\n\n### ').filter(Boolean);
|
|
128
|
+
let trimmed = sections;
|
|
129
|
+
while (trimmed.length > 1 &&
|
|
130
|
+
Buffer.byteLength(header + '\n---\n\n### ' + trimmed.join('\n---\n\n### '), 'utf8') > limit) {
|
|
131
|
+
trimmed = trimmed.slice(1);
|
|
132
|
+
}
|
|
133
|
+
next = header + '\n---\n\n### ' + trimmed.join('\n---\n\n### ');
|
|
134
|
+
if (!next.endsWith('\n')) next += '\n';
|
|
135
|
+
} else {
|
|
136
|
+
next = next.slice(-limit);
|
|
137
|
+
}
|
|
138
|
+
log('info', `Pruned knowledge/agents/${agent}.md to stay under ${limit} bytes`);
|
|
139
|
+
return next;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Heuristic: does this new entry plausibly contradict / supersede / invalidate
|
|
144
|
+
* something in the existing memory? Conservative on purpose — false positives
|
|
145
|
+
* cost an LLM call, false negatives leave stale facts in place. See
|
|
146
|
+
* AGENT_MEMORY_RECONCILE_SIGNAL_RE.
|
|
147
|
+
*/
|
|
148
|
+
function hasReconcileSignals(text) {
|
|
149
|
+
if (!text) return false;
|
|
150
|
+
return AGENT_MEMORY_RECONCILE_SIGNAL_RE.test(String(text));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the LLM prompt for per-agent memory reconciliation. The LLM is asked
|
|
155
|
+
* to identify specific lines/facts in the existing memory that the new entry
|
|
156
|
+
* contradicts, and return literal-string edits in a JSON array.
|
|
157
|
+
*/
|
|
158
|
+
function buildReconcilePrompt(existingMemory, newEntryContent, agent) {
|
|
159
|
+
return `You are reconciling an agent's personal memory file ("knowledge/agents/${agent}.md"). The agent has just produced a new inbox note that may contradict, supersede, or invalidate specific facts the file currently asserts as true. Your job is to identify those specific contradictions and propose surgical edits.
|
|
160
|
+
|
|
161
|
+
## Existing memory file (oldest \u2192 newest, possibly truncated)
|
|
162
|
+
|
|
163
|
+
<existing_memory>
|
|
164
|
+
${existingMemory}
|
|
165
|
+
</existing_memory>
|
|
166
|
+
|
|
167
|
+
## New inbox entry (about to be appended)
|
|
168
|
+
|
|
169
|
+
<new_entry>
|
|
170
|
+
${newEntryContent}
|
|
171
|
+
</new_entry>
|
|
172
|
+
|
|
173
|
+
## Instructions
|
|
174
|
+
|
|
175
|
+
Read the new entry carefully. Does it contradict, supersede, or invalidate any specific lines or facts in the existing memory?
|
|
176
|
+
|
|
177
|
+
If yes, output a JSON array of edits. Each edit must be shaped:
|
|
178
|
+
\`\`\`
|
|
179
|
+
{"old_text": "<exact verbatim substring from the existing memory>", "new_text": "<replacement, or empty string \\"\\" to strike entirely>", "rationale": "<one-sentence reason>"}
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
Rules:
|
|
183
|
+
- \`old_text\` MUST be an EXACT verbatim substring of the existing memory (character-for-character, including punctuation, indentation, and surrounding context). The engine applies edits via literal String.prototype.replace — fuzzy / regex / paraphrased matches will be skipped.
|
|
184
|
+
- Keep \`old_text\` narrowly scoped to the contradicted fact (one line, one bullet, or one short block). Do NOT quote entire sections.
|
|
185
|
+
- Set \`new_text\` to the corrected line(s), or to the empty string \`""\` to strike the line entirely.
|
|
186
|
+
- Only emit edits where the new entry provides clear, factual evidence that the existing assertion is wrong. Speculative or stylistic edits are not allowed.
|
|
187
|
+
- If nothing in the existing memory needs to change, output \`[]\`.
|
|
188
|
+
|
|
189
|
+
Output JSON only. No preamble. No code fences. No explanation outside the JSON.`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse the LLM's edit array, tolerating modest formatting drift (code fences,
|
|
194
|
+
* trailing prose). Returns an array of { old_text, new_text, rationale }.
|
|
195
|
+
* Invalid entries are silently dropped.
|
|
196
|
+
*/
|
|
197
|
+
function parseReconcileEdits(rawText) {
|
|
198
|
+
if (!rawText) return [];
|
|
199
|
+
let txt = String(rawText).trim();
|
|
200
|
+
txt = txt.replace(/^```\w*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
|
|
201
|
+
const start = txt.indexOf('[');
|
|
202
|
+
const end = txt.lastIndexOf(']');
|
|
203
|
+
if (start < 0 || end <= start) return [];
|
|
204
|
+
const slice = txt.slice(start, end + 1);
|
|
205
|
+
let parsed;
|
|
206
|
+
try { parsed = JSON.parse(slice); } catch { return []; }
|
|
207
|
+
if (!Array.isArray(parsed)) return [];
|
|
208
|
+
return parsed
|
|
209
|
+
.filter(e => e && typeof e === 'object' && typeof e.old_text === 'string' && typeof e.new_text === 'string')
|
|
210
|
+
.map(e => ({
|
|
211
|
+
old_text: e.old_text,
|
|
212
|
+
new_text: e.new_text,
|
|
213
|
+
rationale: typeof e.rationale === 'string' ? e.rationale : '',
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Apply reconcile edits to the existing memory content. Uses literal
|
|
219
|
+
* String.indexOf/replace — no regex. Edits whose `old_text` does not match
|
|
220
|
+
* verbatim are skipped with a warning (LLM probably hallucinated). Each
|
|
221
|
+
* applied edit is wrapped with an HTML comment marker so future audits can
|
|
222
|
+
* see what was reconciled and when.
|
|
223
|
+
*/
|
|
224
|
+
function applyReconcileEdits(memoryContent, edits, today) {
|
|
225
|
+
let updated = memoryContent;
|
|
226
|
+
let applied = 0;
|
|
227
|
+
const skipped = [];
|
|
228
|
+
for (const edit of edits) {
|
|
229
|
+
const idx = updated.indexOf(edit.old_text);
|
|
230
|
+
if (idx < 0) {
|
|
231
|
+
skipped.push(edit);
|
|
232
|
+
log('warn', `agent-memory reconcile: old_text not found, skipping edit (${(edit.rationale || '').slice(0, 80)})`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const rationale = (edit.rationale || 'reconciled').replace(/-->/g, '-->').replace(/\s+/g, ' ').trim();
|
|
236
|
+
const marker = `<!-- reconciled ${today}: ${rationale} -->`;
|
|
237
|
+
const replacement = edit.new_text === '' ? marker : `${marker}\n${edit.new_text}`;
|
|
238
|
+
updated = updated.slice(0, idx) + replacement + updated.slice(idx + edit.old_text.length);
|
|
239
|
+
applied++;
|
|
240
|
+
}
|
|
241
|
+
return { updated, applied, skipped };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reconcile-and-append entry point for per-agent memory routing.
|
|
246
|
+
*
|
|
247
|
+
* Decision tree:
|
|
248
|
+
* 1. Agent extraction / known-agent / non-empty content checks — same as
|
|
249
|
+
* appendToAgentMemory.
|
|
250
|
+
* 2. No reconcile signals in the new entry → plain sync append. The LLM
|
|
251
|
+
* cost is reserved for the conservative "this might contradict prior
|
|
252
|
+
* facts" case.
|
|
253
|
+
* 3. Existing memory is trivial (<= 1 KB) → plain sync append.
|
|
254
|
+
* 4. Otherwise: call Haiku via callLLM, ask for surgical edits, apply
|
|
255
|
+
* them, write a .bak of the prior content, then append the new entry.
|
|
256
|
+
* Any LLM failure / 0 applied edits / catastrophic-delete violation
|
|
257
|
+
* falls back to a plain sync append. Reconcile NEVER blocks the
|
|
258
|
+
* consolidation pipeline.
|
|
259
|
+
*
|
|
260
|
+
* Returns a Promise<boolean> for the write outcome (true on success, false
|
|
261
|
+
* on skip). Callers in classifyToKnowledgeBase use fire-and-forget with
|
|
262
|
+
* a .catch() so a hung LLM cannot stall consolidation.
|
|
263
|
+
*/
|
|
264
|
+
function reconcileAndAppendToAgentMemory(item, knownAgents, config) {
|
|
265
|
+
const agent = extractInboxAgent(item);
|
|
266
|
+
if (!agent) return Promise.resolve(false);
|
|
267
|
+
if (agent.startsWith('temp-')) return Promise.resolve(false);
|
|
268
|
+
if (!knownAgents || !knownAgents.has(agent)) return Promise.resolve(false);
|
|
269
|
+
|
|
270
|
+
const content = String(item?.content || '').trim();
|
|
271
|
+
if (!content) return Promise.resolve(false);
|
|
272
|
+
|
|
273
|
+
// Fast path: no contradiction signals → plain sync append. The function
|
|
274
|
+
// still returns a resolved Promise so callers can use a uniform interface.
|
|
275
|
+
if (!hasReconcileSignals(content)) {
|
|
276
|
+
return Promise.resolve(appendToAgentMemory(item, knownAgents));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!fs.existsSync(AGENT_MEMORY_DIR)) {
|
|
280
|
+
try { fs.mkdirSync(AGENT_MEMORY_DIR, { recursive: true }); }
|
|
281
|
+
catch (err) {
|
|
282
|
+
log('warn', `Failed to create agent memory dir: ${err.message}`);
|
|
283
|
+
return Promise.resolve(false);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const memPath = path.join(AGENT_MEMORY_DIR, `${agent}.md`);
|
|
288
|
+
const existingInitial = (fs.existsSync(memPath) ? safeRead(memPath) : '') || '';
|
|
289
|
+
|
|
290
|
+
// Fast path: nothing meaningful to contradict yet.
|
|
291
|
+
if (existingInitial.length <= AGENT_MEMORY_RECONCILE_MIN_EXISTING_BYTES) {
|
|
292
|
+
return Promise.resolve(appendToAgentMemory(item, knownAgents));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Build the entry block exactly as appendToAgentMemory would so reconcile
|
|
296
|
+
// and plain-append produce identical entry framing.
|
|
297
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
298
|
+
const title = titleMatch ? titleMatch[1].trim() : (item.name || 'untitled').replace(/\.md$/, '');
|
|
299
|
+
const entry = `\n\n---\n\n### ${dateStamp()}: ${title}\n_Source: \`notes/inbox/${item.name}\`_\n\n${content}\n`;
|
|
300
|
+
|
|
301
|
+
const memoryForLlm = existingInitial.length > AGENT_MEMORY_RECONCILE_LLM_CAP_BYTES
|
|
302
|
+
? existingInitial.slice(-AGENT_MEMORY_RECONCILE_LLM_CAP_BYTES)
|
|
303
|
+
: existingInitial;
|
|
304
|
+
const prompt = buildReconcilePrompt(memoryForLlm, content, agent);
|
|
305
|
+
const sysPrompt = 'You output ONLY a JSON array of edits, exactly as specified. No preamble. No explanation. No code fences.';
|
|
306
|
+
|
|
307
|
+
let llmCall;
|
|
308
|
+
try {
|
|
309
|
+
llmCall = callLLM(prompt, sysPrompt, {
|
|
310
|
+
timeout: 60000,
|
|
311
|
+
label: 'agent_memory_reconcile',
|
|
312
|
+
model: 'haiku',
|
|
313
|
+
maxTurns: 1,
|
|
314
|
+
direct: true,
|
|
315
|
+
engineConfig: config?.engine,
|
|
316
|
+
});
|
|
317
|
+
} catch (err) {
|
|
318
|
+
log('warn', `agent-memory reconcile: callLLM threw (${err?.message || err}) — plain append`);
|
|
319
|
+
return Promise.resolve(appendToAgentMemory(item, knownAgents));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return Promise.resolve(llmCall).then((result) => {
|
|
323
|
+
try { trackEngineUsage('agent_memory_reconcile', result?.usage); } catch { /* metrics best-effort */ }
|
|
324
|
+
|
|
325
|
+
if (!result || result.missingRuntime || result.code !== 0) {
|
|
326
|
+
log('warn', `agent-memory reconcile: LLM unavailable/failed for ${agent} (code=${result?.code}) — plain append`);
|
|
327
|
+
return appendToAgentMemory(item, knownAgents);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const edits = parseReconcileEdits(result.text || result.raw || '');
|
|
331
|
+
if (edits.length === 0) {
|
|
332
|
+
// LLM said "no contradictions" (or returned garbage) — plain append.
|
|
333
|
+
return appendToAgentMemory(item, knownAgents);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let reconciled = false;
|
|
337
|
+
let lockErr = null;
|
|
338
|
+
try {
|
|
339
|
+
shared.withFileLock(memPath + '.lock', () => {
|
|
340
|
+
const beforeLock = (fs.existsSync(memPath) ? safeRead(memPath) : '') || '';
|
|
341
|
+
const { updated, applied, skipped } = applyReconcileEdits(beforeLock, edits, dateStamp());
|
|
342
|
+
|
|
343
|
+
if (applied === 0) {
|
|
344
|
+
log('warn', `agent-memory reconcile: 0/${edits.length} edits matched for ${agent} — plain append`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Hard safety guard: catastrophic delete.
|
|
349
|
+
const preBytes = Buffer.byteLength(beforeLock, 'utf8');
|
|
350
|
+
const postBytes = Buffer.byteLength(updated, 'utf8');
|
|
351
|
+
if (preBytes > 0 && postBytes < preBytes * AGENT_MEMORY_RECONCILE_MIN_RETAIN_RATIO) {
|
|
352
|
+
log('warn', `agent-memory reconcile: post-edit ${postBytes}B < ${Math.round(AGENT_MEMORY_RECONCILE_MIN_RETAIN_RATIO * 100)}% of pre-edit ${preBytes}B for ${agent} — aborting, plain append`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// One-step backup before destructive write.
|
|
357
|
+
try { safeWrite(memPath + '.bak', beforeLock); }
|
|
358
|
+
catch (err) { log('warn', `agent-memory reconcile: backup write failed for ${agent}: ${err.message}`); }
|
|
359
|
+
|
|
360
|
+
const next = pruneAgentMemoryToBudget(updated + entry, agent);
|
|
361
|
+
safeWrite(memPath, next);
|
|
362
|
+
reconciled = true;
|
|
363
|
+
const skippedCount = skipped.length;
|
|
364
|
+
log('info', `agent-memory reconcile: applied ${applied}/${edits.length} edits${skippedCount ? ` (${skippedCount} skipped)` : ''} to knowledge/agents/${agent}.md`);
|
|
365
|
+
});
|
|
366
|
+
} catch (err) {
|
|
367
|
+
lockErr = err;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (reconciled) return true;
|
|
371
|
+
if (lockErr) log('warn', `agent-memory reconcile: lock/write error for ${agent}: ${lockErr.message} — plain append`);
|
|
372
|
+
return appendToAgentMemory(item, knownAgents);
|
|
373
|
+
}).catch((err) => {
|
|
374
|
+
log('warn', `agent-memory reconcile: LLM promise rejected for ${agent} (${err?.message || err}) — plain append`);
|
|
375
|
+
return appendToAgentMemory(item, knownAgents);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
121
379
|
// Track in-flight LLM consolidation to prevent concurrent runs
|
|
122
380
|
let _consolidationInFlight = false;
|
|
123
381
|
let _consolidationStartedAt = 0;
|
|
@@ -535,7 +793,20 @@ function classifyToKnowledgeBase(items, config) {
|
|
|
535
793
|
// Per-agent memory routing — strict superset of broadcast consolidation.
|
|
536
794
|
// Appends the inbox content to knowledge/agents/<agent>.md when the
|
|
537
795
|
// author is a configured team member (skips temp-* and unknown agents).
|
|
538
|
-
|
|
796
|
+
// When the new entry has contradiction signals, the reconcile pass calls
|
|
797
|
+
// Haiku to identify and rewrite stale facts before the append. Reconcile
|
|
798
|
+
// is fire-and-forget — any failure or hang falls back to plain append
|
|
799
|
+
// inside reconcileAndAppendToAgentMemory; the consolidation pipeline is
|
|
800
|
+
// never blocked on the LLM. (W-mpbi7qus0011bf77)
|
|
801
|
+
try {
|
|
802
|
+
const p = reconcileAndAppendToAgentMemory(item, knownAgents, config);
|
|
803
|
+
if (p && typeof p.catch === 'function') {
|
|
804
|
+
p.catch(err => log('warn', `agent-memory reconcile/append failed: ${err?.message || err}`));
|
|
805
|
+
}
|
|
806
|
+
} catch (err) {
|
|
807
|
+
log('warn', `agent-memory reconcile/append threw: ${err?.message || err}`);
|
|
808
|
+
appendToAgentMemory(item, knownAgents);
|
|
809
|
+
}
|
|
539
810
|
}
|
|
540
811
|
|
|
541
812
|
if (classified > 0) {
|
|
@@ -589,8 +860,17 @@ module.exports = {
|
|
|
589
860
|
// per-agent memory routing
|
|
590
861
|
extractInboxAgent,
|
|
591
862
|
appendToAgentMemory,
|
|
863
|
+
reconcileAndAppendToAgentMemory,
|
|
864
|
+
pruneAgentMemoryToBudget,
|
|
865
|
+
hasReconcileSignals,
|
|
866
|
+
buildReconcilePrompt,
|
|
867
|
+
parseReconcileEdits,
|
|
868
|
+
applyReconcileEdits,
|
|
592
869
|
AGENT_MEMORY_DIR,
|
|
593
870
|
AGENT_MEMORY_BUDGET_BYTES,
|
|
871
|
+
AGENT_MEMORY_RECONCILE_MIN_EXISTING_BYTES,
|
|
872
|
+
AGENT_MEMORY_RECONCILE_LLM_CAP_BYTES,
|
|
873
|
+
AGENT_MEMORY_RECONCILE_MIN_RETAIN_RATIO,
|
|
594
874
|
// exported for testing
|
|
595
875
|
buildConsolidationPrompt,
|
|
596
876
|
consolidateWithLLM,
|
package/engine/dispatch.js
CHANGED
|
@@ -346,6 +346,8 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
346
346
|
FAILURE_CLASS.WORKTREE_PREFLIGHT, // pre-spawn worktree validation — recompute will produce the same failure
|
|
347
347
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR, // W-mp6k7ywi000fa33c — keep-pids cwd is not a real git worktree; re-running won't fix the structural issue
|
|
348
348
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA, // W-mp7i902u000l991f — keep-pids.json failed shape validation; re-running with the same wrong file won't fix it
|
|
349
|
+
FAILURE_CLASS.INVALID_MANAGED_SPAWN, // W-mpbhxg3b000u8411 — managed-spawn.json failed validation; re-running with the same wrong file won't fix it
|
|
350
|
+
FAILURE_CLASS.MANAGED_SPAWN_HEALTHCHECK_FAILED, // W-mpbhxg3b000u8411 — healthcheck timed out; agent must fix the spec or the service it spawned
|
|
349
351
|
]);
|
|
350
352
|
if (neverRetry.has(failureClass)) return false;
|
|
351
353
|
}
|
|
@@ -393,6 +395,23 @@ function isCompletedWorkItemForFailure(item) {
|
|
|
393
395
|
);
|
|
394
396
|
}
|
|
395
397
|
|
|
398
|
+
// W-mpbhxg3b000u8411 — Failure classes that should force-demote a `done` work
|
|
399
|
+
// item back to `failed`. These are the hard sidecar-acceptance and
|
|
400
|
+
// healthcheck rejections fired in engine.js's onAgentClose AFTER
|
|
401
|
+
// runPostCompletionHooks has already flipped the WI to DONE based on the
|
|
402
|
+
// completion-report's verdict:"success". The agent's "I'm done" claim is
|
|
403
|
+
// structurally false when the sidecar didn't pass validation — so the
|
|
404
|
+
// dispatch must be allowed to demote the WI rather than silently ignoring the
|
|
405
|
+
// rejection (incident W-mpbg0jpt0007f7fe). PR-shipped WIs are still
|
|
406
|
+
// protected: the demotion only fires when the WI has no _pr/_prUrl, since
|
|
407
|
+
// these acceptance gates never run for spawns that produced a PR.
|
|
408
|
+
const FORCE_DEMOTE_FAILURE_CLASSES = new Set([
|
|
409
|
+
FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR,
|
|
410
|
+
FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA,
|
|
411
|
+
FAILURE_CLASS.INVALID_MANAGED_SPAWN,
|
|
412
|
+
FAILURE_CLASS.MANAGED_SPAWN_HEALTHCHECK_FAILED,
|
|
413
|
+
]);
|
|
414
|
+
|
|
396
415
|
function readLiveWorkItem(meta) {
|
|
397
416
|
const itemId = meta?.item?.id;
|
|
398
417
|
if (!itemId) return null;
|
|
@@ -533,10 +552,12 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
533
552
|
// Update source work item status on failure + auto-retry with backoff
|
|
534
553
|
const retryableFailure = agentRetryable !== undefined ? agentRetryable : isRetryableFailureReason(reason, failureClass);
|
|
535
554
|
let completedWorkItemFailure = false;
|
|
555
|
+
let liveWi = null;
|
|
536
556
|
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
537
557
|
// If the live item cannot be resolved, keep the existing retry path.
|
|
538
558
|
try {
|
|
539
|
-
|
|
559
|
+
liveWi = readLiveWorkItem(item.meta);
|
|
560
|
+
completedWorkItemFailure = isCompletedWorkItemForFailure(liveWi);
|
|
540
561
|
} catch (e) { log('warn', 'read live work item before retry: ' + e.message); }
|
|
541
562
|
}
|
|
542
563
|
if (result === DISPATCH_RESULT.ERROR && item.meta?.dispatchKey && retryableFailure && !completedWorkItemFailure) {
|
|
@@ -545,7 +566,40 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
545
566
|
|
|
546
567
|
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
547
568
|
if (completedWorkItemFailure) {
|
|
548
|
-
|
|
569
|
+
// W-mpbhxg3b000u8411 — sidecar acceptance / healthcheck rejections fire
|
|
570
|
+
// AFTER runPostCompletionHooks has already flipped the WI to DONE based
|
|
571
|
+
// on the agent's completion-report verdict. The completion-report's
|
|
572
|
+
// success claim is structurally false when the sidecar didn't pass
|
|
573
|
+
// validation, so demote DONE → FAILED with the dispatch's failure
|
|
574
|
+
// class. PR-shipped WIs are explicitly NOT demoted — refuse to unship
|
|
575
|
+
// merged work even if a stale acceptance error somehow arrives late.
|
|
576
|
+
const isPrShipped = !!liveWi && (!!liveWi._pr || !!liveWi._prUrl);
|
|
577
|
+
const isForceDemote = failureClass && FORCE_DEMOTE_FAILURE_CLASSES.has(failureClass) && !isPrShipped;
|
|
578
|
+
if (isForceDemote) {
|
|
579
|
+
try {
|
|
580
|
+
const wiPath = lifecycle().resolveWorkItemPath(item.meta);
|
|
581
|
+
if (wiPath) {
|
|
582
|
+
const classSuffix = ` [${failureClass.toUpperCase().replace(/-/g, '_')}]`;
|
|
583
|
+
const demoteReason = `Non-retryable failure: ${reason || failureClass}${classSuffix}`;
|
|
584
|
+
mutateWorkItems(wiPath, items => {
|
|
585
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
586
|
+
if (wi) {
|
|
587
|
+
wi.status = WI_STATUS.FAILED;
|
|
588
|
+
wi.failReason = demoteReason;
|
|
589
|
+
wi.failedAt = ts();
|
|
590
|
+
wi._failureClass = failureClass;
|
|
591
|
+
wi._lastDispatchResult = DISPATCH_RESULT.ERROR;
|
|
592
|
+
delete wi.completedAt;
|
|
593
|
+
delete wi._noop;
|
|
594
|
+
delete wi._noopReason;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
log('warn', `Demoted WI ${item.meta.item.id} from DONE → FAILED${classSuffix} — sidecar acceptance gate rejected after completion-report claimed success`);
|
|
598
|
+
}
|
|
599
|
+
} catch (e) { log('warn', `force-demote on ${failureClass}: ${e.message}`); }
|
|
600
|
+
} else {
|
|
601
|
+
log('info', `Dispatch error for ${item.meta.item.id} ignored — work item is already completed`);
|
|
602
|
+
}
|
|
549
603
|
} else {
|
|
550
604
|
let retries = (item.meta.item._retryCount || 0);
|
|
551
605
|
try {
|
|
@@ -602,6 +656,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
602
656
|
[FAILURE_CLASS.WORKTREE_PREFLIGHT]: 'worktree preflight rejected (nested in project root or rootDir collapsed to drive root)',
|
|
603
657
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR]: 'keep_processes cwd is not a real git worktree (rerun in a `git worktree add` directory)',
|
|
604
658
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA]: 'keep-pids.json failed shape validation (wrong keys/types/values — see inbox alert for the canonical shape)',
|
|
659
|
+
[FAILURE_CLASS.INVALID_MANAGED_SPAWN]: 'managed-spawn.json failed validation (bad schema, workdir, or allowlist — see inbox alert)',
|
|
660
|
+
[FAILURE_CLASS.MANAGED_SPAWN_HEALTHCHECK_FAILED]: 'managed-spawn spec(s) failed healthcheck within timeout (failing PIDs killed; surviving siblings stay alive)',
|
|
605
661
|
[FAILURE_CLASS.UNKNOWN]: 'unknown error',
|
|
606
662
|
};
|
|
607
663
|
const classLabel = failureClass ? (CLASS_LABELS[failureClass] || failureClass) : '';
|
package/engine/lifecycle.js
CHANGED
|
@@ -945,6 +945,12 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
945
945
|
|
|
946
946
|
function isPrAttachmentRequired(type, item, meta = {}) {
|
|
947
947
|
if (!item?.id || item.skipPr) return false;
|
|
948
|
+
// SETUP (W-mpbi6f2q00104957) is implicitly PR-exempt — the type itself
|
|
949
|
+
// communicates "no PR expected" (boot/configure local infrastructure that
|
|
950
|
+
// mutates state but produces no PR). Callers don't need to also set
|
|
951
|
+
// skipPr:true. This early return beats the explicit-flag fallthrough so a
|
|
952
|
+
// legacy requiresPr:true setup item doesn't trip the contract.
|
|
953
|
+
if (type === WORK_TYPE.SETUP) return false;
|
|
948
954
|
const explicit = item.requiresPr === true
|
|
949
955
|
|| item.prRequired === true
|
|
950
956
|
|| item.requiresPullRequest === true
|
|
@@ -3766,8 +3772,24 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
3766
3772
|
} catch (err) { log('warn', `Scheduled task back-reference: ${err.message}`); }
|
|
3767
3773
|
}
|
|
3768
3774
|
|
|
3769
|
-
// Clean up worktree for non-shared-branch tasks after completion
|
|
3770
|
-
|
|
3775
|
+
// Clean up worktree for non-shared-branch tasks after completion.
|
|
3776
|
+
//
|
|
3777
|
+
// W-mpbinmrh001907e9 — skip worktree removal when this dispatch carried
|
|
3778
|
+
// the `keep_processes` or `managed_spawn` flag (either at the top-level
|
|
3779
|
+
// dispatch meta or nested under `meta.item.meta`). Those flags' acceptance
|
|
3780
|
+
// gates run in engine.js onAgentClose AFTER runPostCompletionHooks
|
|
3781
|
+
// returns, and their workdir validator checks `<cwd>` on disk. If the
|
|
3782
|
+
// worktree is deleted here first, every sidecar pointing at the worktree
|
|
3783
|
+
// fails with a bogus `invalid-workdir: directory does not exist`
|
|
3784
|
+
// rejection. The cleanup sweep (engine/cleanup.js) reaps the worktree
|
|
3785
|
+
// afterwards: when acceptance fails, the >2h age sweep picks it up; when
|
|
3786
|
+
// acceptance succeeds, the managed-spawn cwd anchor in cleanup.js keeps
|
|
3787
|
+
// the worktree alive while the engine-owned services run inside it.
|
|
3788
|
+
const _wiMetaForSkip = meta?.item?.meta || {};
|
|
3789
|
+
const _skipWorktreeRemovalForAcceptance =
|
|
3790
|
+
!!meta?.keep_processes || !!_wiMetaForSkip.keep_processes ||
|
|
3791
|
+
!!meta?.managed_spawn || !!_wiMetaForSkip.managed_spawn;
|
|
3792
|
+
if (meta?.branch && meta?.branchStrategy !== 'shared-branch' && !_skipWorktreeRemovalForAcceptance) {
|
|
3771
3793
|
try {
|
|
3772
3794
|
const project = meta.project || {};
|
|
3773
3795
|
const rootDir = project.localPath ? path.resolve(project.localPath) : null;
|
package/engine/playbook.js
CHANGED
|
@@ -308,6 +308,7 @@ const PLAYBOOK_REQUIRED_VARS = {
|
|
|
308
308
|
'verify': ['task_description'],
|
|
309
309
|
'test': ['item_name'],
|
|
310
310
|
'docs': ['item_id', 'item_name'],
|
|
311
|
+
'setup': ['item_id', 'item_name', 'project_path'],
|
|
311
312
|
'work-item': ['item_id', 'item_name'],
|
|
312
313
|
'meeting-investigate': ['meeting_title', 'agenda'],
|
|
313
314
|
'meeting-debate': ['meeting_title', 'agenda'],
|
|
@@ -859,7 +860,7 @@ function selectPlaybook(workType, item) {
|
|
|
859
860
|
if (workType === WORK_TYPE.FIX && hasPrContext) {
|
|
860
861
|
return 'fix';
|
|
861
862
|
}
|
|
862
|
-
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'docs', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
|
|
863
|
+
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'docs', 'setup', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
|
|
863
864
|
return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
864
865
|
}
|
|
865
866
|
|
package/engine/shared.js
CHANGED
|
@@ -1505,7 +1505,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1505
1505
|
maxEnvVars: 32, // env-object cap per spec
|
|
1506
1506
|
maxAttrsBytes: 2048, // serialized `attrs` blob cap per spec
|
|
1507
1507
|
maxTtlMinutes: 1440, // 24h hard cap on per-spec TTL
|
|
1508
|
-
defaultTtlMinutes:
|
|
1508
|
+
defaultTtlMinutes: 720, // 12h default when spec.ttl_minutes omitted
|
|
1509
1509
|
sweepEvery: 30, // ticks between TTL/dead-PID sweeps
|
|
1510
1510
|
defaultHealthIntervalSec: 1, // healthcheck polling cadence pre-healthy
|
|
1511
1511
|
healthBackoffSec: 30, // healthcheck liveness cadence post-healthy
|
|
@@ -1949,6 +1949,13 @@ const WORK_TYPE = {
|
|
|
1949
1949
|
IMPLEMENT: 'implement', IMPLEMENT_LARGE: 'implement:large', FIX: 'fix', REVIEW: 'review',
|
|
1950
1950
|
VERIFY: 'verify', PLAN: 'plan', PLAN_TO_PRD: 'plan-to-prd', DECOMPOSE: 'decompose',
|
|
1951
1951
|
MEETING: 'meeting', EXPLORE: 'explore', ASK: 'ask', TEST: 'test', DOCS: 'docs',
|
|
1952
|
+
// SETUP (W-mpbi6f2q00104957): "boot/configure local infrastructure" tasks
|
|
1953
|
+
// that mutate state inside a project worktree but produce NO PR. Canonical
|
|
1954
|
+
// example: bootstrapping a constellation dev stack via managed_spawn.
|
|
1955
|
+
// Implicitly PR-exempt — isPrAttachmentRequired short-circuits on this type
|
|
1956
|
+
// so callers don't need to also set skipPr:true. Still worktree-requiring
|
|
1957
|
+
// and still project-required (mirrors implement/fix HTTP-validate path).
|
|
1958
|
+
SETUP: 'setup',
|
|
1952
1959
|
};
|
|
1953
1960
|
|
|
1954
1961
|
// Work types whose dispatch path requires a per-project git worktree. The
|
|
@@ -1976,6 +1983,12 @@ const WORKTREE_REQUIRING_TYPES = new Set([
|
|
|
1976
1983
|
WORK_TYPE.VERIFY,
|
|
1977
1984
|
WORK_TYPE.REVIEW,
|
|
1978
1985
|
WORK_TYPE.DECOMPOSE,
|
|
1986
|
+
// SETUP (W-mpbi6f2q00104957): setup dispatches mutate project state inside
|
|
1987
|
+
// a real worktree (running `bun install`, writing `.env.local`, etc.) so a
|
|
1988
|
+
// project is required even though no PR is produced. Without a project the
|
|
1989
|
+
// worktree resolver falls back to MINIONS_DIR's parent and collapses to a
|
|
1990
|
+
// drive root on installs where MINIONS_DIR sits one level below the root.
|
|
1991
|
+
WORK_TYPE.SETUP,
|
|
1979
1992
|
WORK_TYPE.DOCS,
|
|
1980
1993
|
]);
|
|
1981
1994
|
|
package/engine.js
CHANGED
|
@@ -2526,7 +2526,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2526
2526
|
// during the git ops — otherwise pruneStale() racing on another tick
|
|
2527
2527
|
// could see borrowedBy as orphaned and evict the entry mid-return.
|
|
2528
2528
|
// Skipped when keep_processes PIDs are still alive: the worktree may be
|
|
2529
|
-
// the cwd of a left-running dev server or watcher.
|
|
2529
|
+
// the cwd of a left-running dev server or watcher. W-mpbinmrh001907e9:
|
|
2530
|
+
// also skipped when managed_spawn just placed live services with cwd
|
|
2531
|
+
// inside the worktree — `git reset --hard` + `git clean -fd` here would
|
|
2532
|
+
// corrupt those services' state and detach them from the branch they
|
|
2533
|
+
// expect.
|
|
2530
2534
|
if (effectiveResult === DISPATCH_RESULT.SUCCESS && worktreePath && fs.existsSync(worktreePath)) {
|
|
2531
2535
|
let _keepPidsAlive = false;
|
|
2532
2536
|
try {
|
|
@@ -2534,10 +2538,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2534
2538
|
const _anchorRes = _ks.getActiveAnchorPidsForAgent(agentId);
|
|
2535
2539
|
if (_anchorRes && _anchorRes.pids && _anchorRes.pids.size > 0) _keepPidsAlive = true;
|
|
2536
2540
|
} catch (_e) { /* keep-process-sweep import optional — fall through */ }
|
|
2541
|
+
// W-mpbinmrh001907e9 — when managed-spawn accepted + spawned services
|
|
2542
|
+
// this dispatch, treat the worktree the same as a keep_processes-anchored
|
|
2543
|
+
// one: skip pool return + evict any stale pool entry. We rely on the
|
|
2544
|
+
// in-scope spawn count (truth-at-this-moment) rather than a state-file
|
|
2545
|
+
// re-read because the spawn happened seconds ago in this same handler.
|
|
2546
|
+
const _managedSpawnAlive = Array.isArray(managedSpawnSpawned) && managedSpawnSpawned.length > 0;
|
|
2537
2547
|
|
|
2538
2548
|
const _projForReturn = project?.name || 'default';
|
|
2539
2549
|
const _poolSizeReturn = worktreePool.getProjectPoolSize(_projForReturn, config);
|
|
2540
|
-
if (!_keepPidsAlive && _poolSizeReturn > 0) {
|
|
2550
|
+
if (!_keepPidsAlive && !_managedSpawnAlive && _poolSizeReturn > 0) {
|
|
2541
2551
|
try {
|
|
2542
2552
|
const _mainRefRet = sanitizeBranch(shared.resolveMainBranch(rootDir, project?.mainBranch));
|
|
2543
2553
|
await shared.shellSafeGit(['reset', '--hard', 'HEAD'], { ..._gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
@@ -2560,10 +2570,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2560
2570
|
log('warn', `worktree-pool: return failed for ${worktreePath}: ${returnErr.message} — evicting from pool`);
|
|
2561
2571
|
worktreePool.evictEntry(worktreePath, 'return-git-failed');
|
|
2562
2572
|
}
|
|
2563
|
-
} else if (_keepPidsAlive) {
|
|
2564
|
-
// Skip the pool — the worktree is in use by left-running processes
|
|
2565
|
-
//
|
|
2566
|
-
|
|
2573
|
+
} else if (_keepPidsAlive || _managedSpawnAlive) {
|
|
2574
|
+
// Skip the pool — the worktree is in use by left-running processes
|
|
2575
|
+
// (keep_processes PIDs or managed-spawn services). Make sure no
|
|
2576
|
+
// stale entry lingers (defensive).
|
|
2577
|
+
const _reason = _managedSpawnAlive ? 'managed-spawn-alive' : 'keep-processes-alive';
|
|
2578
|
+
worktreePool.evictEntry(worktreePath, _reason);
|
|
2567
2579
|
}
|
|
2568
2580
|
}
|
|
2569
2581
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1972",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Playbook: Setup
|
|
2
|
+
|
|
3
|
+
You are {{agent_name}}, the {{agent_role}} on the {{project_name}} project.
|
|
4
|
+
TEAM ROOT: {{team_root}}
|
|
5
|
+
|
|
6
|
+
Repository ID is injected as `{{ado_project}}` and `{{repo_name}}` template variables.
|
|
7
|
+
Repo: {{repo_name}} | Org: {{ado_org}} | Project: {{ado_project}}
|
|
8
|
+
|
|
9
|
+
## Your Task
|
|
10
|
+
|
|
11
|
+
Setup task **{{item_id}}: {{item_name}}**
|
|
12
|
+
- Priority: {{item_priority}}
|
|
13
|
+
- Description: {{item_description}}
|
|
14
|
+
|
|
15
|
+
{{additional_context}}
|
|
16
|
+
|
|
17
|
+
{{references}}
|
|
18
|
+
|
|
19
|
+
{{acceptance_criteria}}
|
|
20
|
+
|
|
21
|
+
## What "setup" means
|
|
22
|
+
|
|
23
|
+
`setup` work items boot or configure local infrastructure inside a project
|
|
24
|
+
worktree. Canonical examples: bootstrap a dev stack (`bun install`, `npm
|
|
25
|
+
install`, write `.env.local`, seed local databases), launch a long-running
|
|
26
|
+
dev server via `meta.managed_spawn`, prime a sandbox/emulator. They mutate
|
|
27
|
+
state but **produce no pull request**.
|
|
28
|
+
|
|
29
|
+
If your assignment requires committing files to the tracked repo (real code
|
|
30
|
+
or doc changes), it's the wrong type — escalate so the dispatcher reclassifies
|
|
31
|
+
it as `implement` / `fix` / `docs` instead of forcing a PR-less merge.
|
|
32
|
+
|
|
33
|
+
## Working directory
|
|
34
|
+
|
|
35
|
+
You are running inside a real project worktree. Confirm the path before doing
|
|
36
|
+
anything filesystem-sensitive:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# PowerShell
|
|
40
|
+
echo $env:MINIONS_AGENT_CWD
|
|
41
|
+
pwd
|
|
42
|
+
|
|
43
|
+
# bash/zsh
|
|
44
|
+
echo "$MINIONS_AGENT_CWD"
|
|
45
|
+
pwd
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`MINIONS_AGENT_CWD` is the engine-resolved worktree root and is the
|
|
49
|
+
authoritative path for cwd-sensitive commands. If it disagrees with `pwd`,
|
|
50
|
+
prefer `MINIONS_AGENT_CWD` and `cd` there before continuing.
|
|
51
|
+
|
|
52
|
+
## Project Scope
|
|
53
|
+
|
|
54
|
+
Primary repo: **{{repo_name}}** ({{ado_org}}/{{ado_project}}) at `{{project_path}}`
|
|
55
|
+
|
|
56
|
+
## No PR expected
|
|
57
|
+
|
|
58
|
+
This is the defining contract of `setup`:
|
|
59
|
+
|
|
60
|
+
- **Do NOT run `git push`.** No remote branch is needed.
|
|
61
|
+
- **Do NOT run `gh pr create`** (or any host-specific PR creation command).
|
|
62
|
+
- **Do NOT create a new branch** beyond the engine's pre-created worktree branch.
|
|
63
|
+
- The engine's PR-attachment contract is short-circuited for `type: "setup"`,
|
|
64
|
+
so completing `done` with no PR is the correct outcome.
|
|
65
|
+
|
|
66
|
+
If you accidentally produce code changes that need review, stop, leave them
|
|
67
|
+
uncommitted, and report what happened in the completion report so the human
|
|
68
|
+
can re-dispatch as `implement` or `fix`.
|
|
69
|
+
|
|
70
|
+
## Managed spawn / keep-process hints
|
|
71
|
+
|
|
72
|
+
If the dispatcher set `meta.managed_spawn` or `meta.keep_processes` on this
|
|
73
|
+
work item, the engine auto-injects the corresponding contract block above
|
|
74
|
+
this playbook. Follow the contract block verbatim — it tells you exactly
|
|
75
|
+
which sidecar file to write (`agents/<id>/managed-spawn.json` or
|
|
76
|
+
`agents/<id>/keep-pids.json`), the schema, and the engine-side reap behavior.
|
|
77
|
+
Both injectors are orthogonal to work-item type, so they work the same here
|
|
78
|
+
as in `implement`.
|
|
79
|
+
|
|
80
|
+
## Health Check
|
|
81
|
+
|
|
82
|
+
Before starting work, run `git status` to confirm the worktree is clean.
|
|
83
|
+
If it's dirty or on an unexpected branch, report and stop.
|
|
84
|
+
|
|
85
|
+
## Working Style
|
|
86
|
+
|
|
87
|
+
Use subagents only for genuinely parallel, independent tasks. For sequential
|
|
88
|
+
work, single-file edits, searches, and file reads, work directly.
|
|
89
|
+
|
|
90
|
+
## Validation
|
|
91
|
+
|
|
92
|
+
Prove the setup actually worked before declaring success — the agent's
|
|
93
|
+
completion is the only signal the engine and user get that the infrastructure
|
|
94
|
+
is up:
|
|
95
|
+
|
|
96
|
+
- Use the project's documented commands (`CLAUDE.md`, README, `package.json`
|
|
97
|
+
scripts) — don't invent ones.
|
|
98
|
+
- For long-running services, hit the documented health endpoint / health
|
|
99
|
+
command before exiting (e.g. `curl http://localhost:<port>/health`).
|
|
100
|
+
- Capture the exact commands you ran in the completion report. Do not write
|
|
101
|
+
"setup completed" without naming what ran.
|
|
102
|
+
|
|
103
|
+
Long installs and dev-stack boots may be quiet for several minutes. Let the
|
|
104
|
+
normal CLI command run naturally; do not add artificial heartbeat output.
|
|
105
|
+
|
|
106
|
+
## When to Stop
|
|
107
|
+
|
|
108
|
+
Your task is complete when the requested infrastructure is up, validated by
|
|
109
|
+
the project's own health command, and the completion report names the
|
|
110
|
+
commands you ran and the URLs / PIDs of anything left running. There is no
|
|
111
|
+
PR to wait for.
|
|
112
|
+
|
|
113
|
+
Do NOT remove the worktree — the engine handles cleanup automatically.
|
package/prompts/cc-system.md
CHANGED
|
@@ -145,10 +145,10 @@ curl -s http://localhost:{{dashboard_port}}/api/status
|
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
**Required fields per endpoint** — the server returns `{ error: "..." }` if missing. Common cases:
|
|
148
|
-
- `POST /api/work-items`: `title` REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). `type` defaults to `implement`; valid values: `fix`, `implement`, `implement:large`, `explore`, `ask`, `review`, `test`, `verify`. Agent hint via `agent` (string) or `agents` (array).
|
|
149
|
-
- Exempt from the `project` requirement (these run rootless or via central paths): `ask`, `explore`, `plan`, `plan-to-prd`, `meeting`. (`docs` is intentionally NOT exempt — it's write-capable and lands in `WORKTREE_REQUIRING_TYPES`, so it needs a real project worktree. For minions-repo docs work, pass `project: "minions"` explicitly.) Every other type needs a project worktree, so the server rejects project-less creates with `400 { error, knownProjects }` when ≠1 project is configured.
|
|
148
|
+
- `POST /api/work-items`: `title` REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). `type` defaults to `implement`; valid values: `fix`, `implement`, `implement:large`, `setup`, `explore`, `ask`, `review`, `test`, `verify`. Agent hint via `agent` (string) or `agents` (array).
|
|
149
|
+
- Exempt from the `project` requirement (these run rootless or via central paths): `ask`, `explore`, `plan`, `plan-to-prd`, `meeting`. (`docs` is intentionally NOT exempt — it's write-capable and lands in `WORKTREE_REQUIRING_TYPES`, so it needs a real project worktree. For minions-repo docs work, pass `project: "minions"` explicitly.) `setup` is also in the project-required set — it operates inside a real project worktree but produces no PR. Every other type needs a project worktree, so the server rejects project-less creates with `400 { error, knownProjects }` when ≠1 project is configured.
|
|
150
150
|
- **`meta.keep_processes: true`** — opt-in flag that lets the agent leave specific descendant PIDs running after it exits (default: engine reaps EVERYTHING the agent spawned). **Set this whenever the user's intent is to leave a process alive after the agent finishes** — e.g. "spin up the dev server and exit", "start the watcher and leave it running", "set up my dev env", "keep the emulator open", "launch the daemon for me", "boot the constellation host and disconnect". Don't set it for normal build/test/run-once tasks (`npm test`, `npm run build`, one-shot scripts) — those should be reaped. Also accepts optional `meta.keep_processes_ttl_minutes` (default 60, hard-cap 1440 = 24h). When you set this flag, also make the WI title/description say something like "leave the dev server running" so the agent knows to write `agents/<id>/keep-pids.json` before exiting (the playbook injects the contract automatically when the flag is on). Example: `-d '{"title":"Spin up Constellation dev env and leave server running","type":"implement","project":"constellation","description":"Run bun install + bun run dev. Leave the dev server (port 5173) and Constellation host (port 3001) running after you exit so the user can iterate.","meta":{"keep_processes":true,"keep_processes_ttl_minutes":240}}'`. Inspect / kill kept PIDs anytime via `GET /api/keep-processes` and `POST /api/keep-processes/kill`.
|
|
151
|
-
- **`skipPr: true`** — opt-in flag that tells the engine NOT to enforce the PR-attachment contract for this work item, so the WI can complete `done` without the missing-PR hard-fail. **Set this when the dispatch mutates state OUTSIDE any tracked git repo and therefore cannot produce a PR** — e.g. cleaning `~/.claude/skills/`, editing runtime config under `~/.config/`, resetting the dashboard cache, mutating engine JSON state files (`engine/*.json`) the engine itself owns, or local tooling installs. **Do NOT set it for any task that touches a tracked repo's source** — even one-line diffs in a real repo should produce a PR. Type-selection rule of thumb: prefer `type: "explore"` for genuinely read-only tasks (rootless, no worktree, no PR contract); use `skipPr: true` when the task is write-side mutation but the writes don't land in a git repo. Example: `-d '{"title":"
|
|
151
|
+
- **`skipPr: true`** — opt-in flag that tells the engine NOT to enforce the PR-attachment contract for this work item, so the WI can complete `done` without the missing-PR hard-fail. **Set this when the dispatch mutates state OUTSIDE any tracked git repo and therefore cannot produce a PR** — e.g. cleaning `~/.claude/skills/`, editing runtime config under `~/.config/`, resetting the dashboard cache, mutating engine JSON state files (`engine/*.json`) the engine itself owns, or local tooling installs. **Do NOT set it for any task that touches a tracked repo's source** — even one-line diffs in a real repo should produce a PR. Type-selection rule of thumb: prefer `type: "setup"` for infra/dev-env bootstrap tasks that mutate project state but produce no PR (it's implicitly PR-exempt — no `skipPr` needed); prefer `type: "explore"` for genuinely read-only tasks (rootless, no worktree, no PR contract); use `skipPr: true` only when the task is write-side mutation but the writes don't land in a git repo and `setup` doesn't fit (e.g. cleaning user-machine state outside any repo). Example: `-d '{"title":"Bootstrap Constellation dev stack","type":"setup","project":"constellation","description":"Run bun install + bun run dev and leave the dev server running.","meta":{"managed_spawn":true}}'`.
|
|
152
152
|
- **`oneShot: true`** — opt-in flag for one-off human-initiated dispatches that should NOT enroll the discovered PR into the engine's automatic review/fix loop. The PR is still tracked (status + comments are polled normally) but `discoverFromPrs` skips it for review/fix dispatch. **Set this when the user's intent is "do this single action against an existing PR, then stop"** — e.g. "review PR #2533 once", "rebase PR #2540 once and exit", "post a fix-summary comment on PR #2519". Don't set it for normal feature/fix work where the PR should keep cycling through review/fix until merged. Example: `-d '{"title":"One-off review of PR #2533","type":"review","project":"minions","description":"Single review pass on github:yemi33/minions#2533. Do not re-dispatch on subsequent comments.","oneShot":true}'`.
|
|
153
153
|
- `POST /api/notes`: `title`, `what` REQUIRED.
|
|
154
154
|
- `POST /api/knowledge`: `category`, `title`, `content` REQUIRED. Categories: `architecture`, `conventions`, `project-notes`, `build-reports`, `reviews`.
|
package/routing.md
CHANGED
|
@@ -20,6 +20,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
|
|
|
20
20
|
| decompose | ripley | rebecca |
|
|
21
21
|
| meeting | ripley | lambert |
|
|
22
22
|
| docs | lambert | _any_ |
|
|
23
|
+
| setup | dallas | _any_ |
|
|
23
24
|
|
|
24
25
|
Notes:
|
|
25
26
|
- `_author_` means route to the PR author
|