@yemi33/minions 0.1.525 → 0.1.527
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 +7 -1
- package/dashboard/js/render-work-items.js +4 -4
- package/dashboard.js +3 -3
- package/engine/ado.js +1 -1
- package/engine/cleanup.js +3 -9
- package/engine/cli.js +4 -4
- package/engine/consolidation.js +2 -2
- package/engine/dispatch.js +4 -15
- package/engine/github.js +1 -1
- package/engine/lifecycle.js +5 -30
- package/engine/llm.js +2 -2
- package/engine/meeting.js +25 -25
- package/engine/playbook.js +3 -3
- package/engine/scheduler.js +3 -3
- package/engine/shared.js +30 -0
- package/engine/spawn-agent.js +3 -3
- package/engine.js +12 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.527 (2026-04-07)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- Replace direct safeJson reads of work-items.json with getWorkItems() (#424)
|
|
7
|
+
|
|
8
|
+
## 0.1.526 (2026-04-07)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- Add Concurrency & Lock Ordering section to CLAUDE.md (#409)
|
|
6
12
|
- 9 test isolation verification tests — run last to detect pollution
|
|
7
13
|
|
|
8
14
|
### Fixes
|
|
@@ -175,8 +175,8 @@ async function submitWorkItemEdit(id, source, e) {
|
|
|
175
175
|
const refsRaw = document.getElementById('wi-edit-refs')?.value || '';
|
|
176
176
|
const references = refsRaw.split('\n').filter(function(l) { return l.trim(); }).map(function(l) {
|
|
177
177
|
var parts = l.split('|').map(function(s) { return s.trim(); });
|
|
178
|
-
return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
|
|
179
|
-
});
|
|
178
|
+
return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
|
|
179
|
+
}).filter(function(r) { return r.url; });
|
|
180
180
|
const acRaw = document.getElementById('wi-edit-ac')?.value || '';
|
|
181
181
|
const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
|
|
182
182
|
if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title is required'); return; }
|
|
@@ -387,8 +387,8 @@ async function _submitCreateWorkItem(e) {
|
|
|
387
387
|
const refsRaw = document.getElementById('wi-new-refs')?.value || '';
|
|
388
388
|
const references = refsRaw.split('\n').filter(l => l.trim()).map(l => {
|
|
389
389
|
const parts = l.split('|').map(s => s.trim());
|
|
390
|
-
return { url: parts[0], title: parts[1] || parts[0], type: parts[2] || 'link' };
|
|
391
|
-
});
|
|
390
|
+
return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
|
|
391
|
+
}).filter(r => r.url);
|
|
392
392
|
|
|
393
393
|
try {
|
|
394
394
|
const body = { title, description: desc, type, priority };
|
package/dashboard.js
CHANGED
|
@@ -192,8 +192,8 @@ async function checkNpmVersion() {
|
|
|
192
192
|
_npmVersionCache = { latest: version || null, checkedAt: new Date().toISOString() };
|
|
193
193
|
_npmVersionCacheTs = now;
|
|
194
194
|
} catch (e) {
|
|
195
|
-
console.error('[version-check] npm view failed:', e.message?.split('\n')[0]);
|
|
196
|
-
_npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')[0] || 'check failed' };
|
|
195
|
+
console.error('[version-check] npm view failed:', e.message?.split('\n')?.[0]);
|
|
196
|
+
_npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: e.message?.split('\n')?.[0] || 'check failed' };
|
|
197
197
|
}
|
|
198
198
|
return _npmVersionCache;
|
|
199
199
|
}
|
|
@@ -3816,7 +3816,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3816
3816
|
// Determine project
|
|
3817
3817
|
reloadConfig();
|
|
3818
3818
|
const projects = shared.getProjects(CONFIG);
|
|
3819
|
-
const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : projects[0];
|
|
3819
|
+
const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : (projects[0] || null);
|
|
3820
3820
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
|
|
3821
3821
|
|
|
3822
3822
|
// Extract PR number from URL
|
package/engine/ado.js
CHANGED
|
@@ -173,7 +173,7 @@ async function pollPrStatus(config) {
|
|
|
173
173
|
const headCommit = prData.lastMergeSourceCommit?.commitId || prData.sourceRefName || '';
|
|
174
174
|
if (headCommit && pr._adoHeadCommit !== headCommit) {
|
|
175
175
|
if (pr._adoHeadCommit) { // skip first detection — only track changes
|
|
176
|
-
pr.lastPushedAt =
|
|
176
|
+
pr.lastPushedAt = ts();
|
|
177
177
|
}
|
|
178
178
|
pr._adoHeadCommit = headCommit;
|
|
179
179
|
updated = true;
|
package/engine/cleanup.js
CHANGED
|
@@ -308,15 +308,9 @@ function runCleanup(config, verbose = false) {
|
|
|
308
308
|
// Collect all work item IDs across all sources
|
|
309
309
|
const allWiIds = new Set();
|
|
310
310
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
} catch (e) { log('warn', 'read
|
|
314
|
-
for (const project of projects) {
|
|
315
|
-
try {
|
|
316
|
-
const projItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
317
|
-
projItems.forEach(w => allWiIds.add(w.id));
|
|
318
|
-
} catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
|
|
319
|
-
}
|
|
311
|
+
const allItems = queries.getWorkItems();
|
|
312
|
+
allItems.forEach(w => allWiIds.add(w.id));
|
|
313
|
+
} catch (e) { log('warn', 'read work items for orphan check: ' + e.message); }
|
|
320
314
|
|
|
321
315
|
let changed = false;
|
|
322
316
|
for (const queue of ['pending', 'active']) {
|
package/engine/cli.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
|
-
const { safeRead, safeJson, safeWrite, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
9
|
+
const { safeRead, safeJson, safeWrite, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { getConfig, getControl, getDispatch, getAgentStatus,
|
|
12
12
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLANS_DIR, PRD_DIR, CONTROL_PATH, DISPATCH_PATH } = queries;
|
|
@@ -174,7 +174,7 @@ const commands = {
|
|
|
174
174
|
if (wi && wi.status !== WI_STATUS.DISPATCHED) {
|
|
175
175
|
wi.status = WI_STATUS.DISPATCHED;
|
|
176
176
|
wi.dispatched_to = wi.dispatched_to || agentId;
|
|
177
|
-
wi.dispatched_at = wi.dispatched_at ||
|
|
177
|
+
wi.dispatched_at = wi.dispatched_at || ts();
|
|
178
178
|
safeWrite(wiPath, wiItems);
|
|
179
179
|
}
|
|
180
180
|
} catch (err) { console.log(` Warning: failed to sync work item status: ${err.message}`); }
|
|
@@ -273,8 +273,8 @@ const commands = {
|
|
|
273
273
|
const wi = items.find(w => w.id === item.meta.item.id);
|
|
274
274
|
if (wi) {
|
|
275
275
|
wi.status = status;
|
|
276
|
-
if (isSuccess) { wi.completedAt =
|
|
277
|
-
else { wi.failedAt =
|
|
276
|
+
if (isSuccess) { wi.completedAt = ts(); delete wi.failReason; }
|
|
277
|
+
else { wi.failedAt = ts(); wi.failReason = 'Completed while engine was down'; }
|
|
278
278
|
safeWrite(wiPath, items);
|
|
279
279
|
}
|
|
280
280
|
}
|
package/engine/consolidation.js
CHANGED
|
@@ -9,7 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const shared = require('./shared');
|
|
11
11
|
const { safeRead, safeWrite, safeUnlink, runFile, cleanChildEnv,
|
|
12
|
-
parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES, log, dateStamp } = shared;
|
|
12
|
+
parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES, log, ts, dateStamp } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
14
14
|
const queries = require('./queries');
|
|
15
15
|
const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR, MINIONS_DIR,
|
|
@@ -440,7 +440,7 @@ function classifyToKnowledgeBase(items) {
|
|
|
440
440
|
const dir = path.join(KNOWLEDGE_DIR, cat);
|
|
441
441
|
if (fs.existsSync(dir)) count += fs.readdirSync(dir).length;
|
|
442
442
|
}
|
|
443
|
-
safeWrite(path.join(ENGINE_DIR, 'kb-checkpoint.json'), JSON.stringify({ count, updatedAt:
|
|
443
|
+
safeWrite(path.join(ENGINE_DIR, 'kb-checkpoint.json'), JSON.stringify({ count, updatedAt: ts() }));
|
|
444
444
|
} catch (err) { log('warn', `KB checkpoint: ${err.message}`); }
|
|
445
445
|
}
|
|
446
446
|
|
package/engine/dispatch.js
CHANGED
|
@@ -126,14 +126,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
126
126
|
if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
|
|
127
127
|
let retries = (item.meta.item._retryCount || 0);
|
|
128
128
|
try {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
const items = safeJson(wiPath);
|
|
132
|
-
if (items && Array.isArray(items)) {
|
|
133
|
-
const wi = items.find(i => i.id === item.meta.item.id);
|
|
134
|
-
if (wi) retries = wi._retryCount || 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
129
|
+
const wi = queries.getWorkItems().find(i => i.id === item.meta.item.id);
|
|
130
|
+
if (wi) retries = wi._retryCount || 0;
|
|
137
131
|
} catch (e) { log('warn', 'read retry count: ' + e.message); }
|
|
138
132
|
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
139
133
|
if (retryableFailure && retries < maxRetries) {
|
|
@@ -178,13 +172,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
178
172
|
const config = getConfig();
|
|
179
173
|
const failedId = item.meta.item.id;
|
|
180
174
|
const blockedItems = [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
items.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
184
|
-
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
185
|
-
}
|
|
186
|
-
const centralItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
187
|
-
centralItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
175
|
+
const allItems = queries.getWorkItems(config);
|
|
176
|
+
allItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
|
|
188
177
|
.forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
|
|
189
178
|
|
|
190
179
|
writeInboxAlert(`failed-${failedId}`,
|
package/engine/github.js
CHANGED
|
@@ -217,7 +217,7 @@ async function pollPrStatus(config) {
|
|
|
217
217
|
// Track head SHA changes to detect new pushes (used for review re-dispatch gating)
|
|
218
218
|
if (prData.head?.sha && pr.headSha !== prData.head.sha) {
|
|
219
219
|
pr.headSha = prData.head.sha;
|
|
220
|
-
pr.lastPushedAt =
|
|
220
|
+
pr.lastPushedAt = ts();
|
|
221
221
|
updated = true;
|
|
222
222
|
}
|
|
223
223
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -30,20 +30,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
30
30
|
const projects = shared.getProjects(config);
|
|
31
31
|
|
|
32
32
|
// Collect work items from ALL projects + central (PRD items can be in either)
|
|
33
|
-
|
|
34
|
-
for (const p of projects) {
|
|
35
|
-
try {
|
|
36
|
-
const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
|
|
37
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
38
|
-
} catch { /* optional */ }
|
|
39
|
-
}
|
|
40
|
-
// Also check central work-items.json (for no-project setups)
|
|
41
|
-
try {
|
|
42
|
-
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
43
|
-
for (const w of central) {
|
|
44
|
-
if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
|
|
45
|
-
}
|
|
46
|
-
} catch { /* optional */ }
|
|
33
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
47
34
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile && w.itemType !== 'pr' && w.itemType !== 'verify');
|
|
48
35
|
if (planItems.length === 0) return;
|
|
49
36
|
|
|
@@ -121,7 +108,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
121
108
|
``,
|
|
122
109
|
`**Project:** ${plan.project || 'Unknown'}`,
|
|
123
110
|
`**Strategy:** ${plan.branch_strategy || 'parallel'}`,
|
|
124
|
-
`**Completed:** ${
|
|
111
|
+
`**Completed:** ${ts().slice(0, 16).replace('T', ' ')}`,
|
|
125
112
|
`**Runtime:** ${runtimeMin >= 60 ? Math.floor(runtimeMin / 60) + 'h ' + (runtimeMin % 60) + 'm' : runtimeMin + 'm'}`,
|
|
126
113
|
``,
|
|
127
114
|
`## Results`,
|
|
@@ -330,19 +317,7 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
330
317
|
if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
331
318
|
|
|
332
319
|
// Collect work items for this plan
|
|
333
|
-
|
|
334
|
-
for (const p of projects) {
|
|
335
|
-
try {
|
|
336
|
-
const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
|
|
337
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
338
|
-
} catch { /* optional */ }
|
|
339
|
-
}
|
|
340
|
-
try {
|
|
341
|
-
const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
342
|
-
for (const w of central) {
|
|
343
|
-
if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
|
|
344
|
-
}
|
|
345
|
-
} catch { /* optional */ }
|
|
320
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
346
321
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
|
|
347
322
|
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
348
323
|
|
|
@@ -1136,7 +1111,7 @@ function handleDecompositionResult(stdout, meta, config) {
|
|
|
1136
1111
|
sourcePlan: p.sourcePlan,
|
|
1137
1112
|
branchStrategy: p.branchStrategy,
|
|
1138
1113
|
featureBranch: p.featureBranch,
|
|
1139
|
-
created:
|
|
1114
|
+
created: ts(),
|
|
1140
1115
|
createdBy: 'decomposition',
|
|
1141
1116
|
});
|
|
1142
1117
|
}
|
|
@@ -1162,7 +1137,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1162
1137
|
if (isSuccess && sessionId && agentId && !agentId.startsWith('temp-')) {
|
|
1163
1138
|
try {
|
|
1164
1139
|
shared.safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
1165
|
-
sessionId, dispatchId: dispatchItem.id, savedAt:
|
|
1140
|
+
sessionId, dispatchId: dispatchItem.id, savedAt: ts(),
|
|
1166
1141
|
branch: dispatchItem.meta?.branch || null,
|
|
1167
1142
|
});
|
|
1168
1143
|
} catch (err) { log('warn', `Session save: ${err.message}`); }
|
package/engine/llm.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { safeWrite, safeUnlink, uid, runFile, cleanChildEnv, parseStreamJsonOutput, mutateJsonFileLocked } = shared;
|
|
8
|
+
const { safeWrite, safeUnlink, uid, ts, runFile, cleanChildEnv, parseStreamJsonOutput, mutateJsonFileLocked } = shared;
|
|
9
9
|
|
|
10
10
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
11
11
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
@@ -28,7 +28,7 @@ function trackEngineUsage(category, usage) {
|
|
|
28
28
|
cat.cacheRead += usage.cacheRead || 0;
|
|
29
29
|
cat.cacheCreation = (cat.cacheCreation || 0) + (usage.cacheCreation || 0);
|
|
30
30
|
|
|
31
|
-
const today =
|
|
31
|
+
const today = ts().slice(0, 10);
|
|
32
32
|
if (!metrics._daily) metrics._daily = {};
|
|
33
33
|
if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0 };
|
|
34
34
|
const daily = metrics._daily[today];
|
package/engine/meeting.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
|
-
const { safeJson, safeWrite, safeRead, uid, log, ENGINE_DEFAULTS, WORK_TYPE, DISPATCH_RESULT } = shared;
|
|
9
|
+
const { safeJson, safeWrite, safeRead, uid, log, ts, ENGINE_DEFAULTS, WORK_TYPE, DISPATCH_RESULT } = shared;
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { getDispatch, getConfig } = queries;
|
|
12
12
|
const { renderPlaybook } = require('./playbook');
|
|
@@ -51,8 +51,8 @@ function createMeeting({ title, agenda, participants }) {
|
|
|
51
51
|
round: 1,
|
|
52
52
|
participants: participants || [],
|
|
53
53
|
createdBy: 'human',
|
|
54
|
-
createdAt:
|
|
55
|
-
roundStartedAt:
|
|
54
|
+
createdAt: ts(),
|
|
55
|
+
roundStartedAt: ts(),
|
|
56
56
|
findings: {},
|
|
57
57
|
debate: {},
|
|
58
58
|
conclusion: null,
|
|
@@ -209,16 +209,16 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
|
209
209
|
const content = rawContent;
|
|
210
210
|
|
|
211
211
|
if (roundName === 'investigate') {
|
|
212
|
-
meeting.findings[agentId] = { content, submittedAt:
|
|
213
|
-
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'finding', content, at:
|
|
212
|
+
meeting.findings[agentId] = { content, submittedAt: ts() };
|
|
213
|
+
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'finding', content, at: ts() });
|
|
214
214
|
} else if (roundName === 'debate') {
|
|
215
|
-
meeting.debate[agentId] = { content, submittedAt:
|
|
216
|
-
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'debate', content, at:
|
|
215
|
+
meeting.debate[agentId] = { content, submittedAt: ts() };
|
|
216
|
+
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'debate', content, at: ts() });
|
|
217
217
|
} else if (roundName === 'conclude') {
|
|
218
|
-
meeting.conclusion = { content, agent: agentId, submittedAt:
|
|
219
|
-
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'conclusion', content, at:
|
|
218
|
+
meeting.conclusion = { content, agent: agentId, submittedAt: ts() };
|
|
219
|
+
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'conclusion', content, at: ts() });
|
|
220
220
|
meeting.status = 'completed';
|
|
221
|
-
meeting.completedAt =
|
|
221
|
+
meeting.completedAt = ts();
|
|
222
222
|
|
|
223
223
|
// Write transcript to inbox so agents learn from it (slug-based dedup)
|
|
224
224
|
try {
|
|
@@ -246,12 +246,12 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
|
|
|
246
246
|
if (meeting.status === 'investigating') {
|
|
247
247
|
meeting.status = 'debating';
|
|
248
248
|
meeting.round = 2;
|
|
249
|
-
meeting.roundStartedAt =
|
|
249
|
+
meeting.roundStartedAt = ts();
|
|
250
250
|
log('info', `Meeting ${meetingId}: all findings in — advancing to debate`);
|
|
251
251
|
} else if (meeting.status === 'debating') {
|
|
252
252
|
meeting.status = 'concluding';
|
|
253
253
|
meeting.round = 3;
|
|
254
|
-
meeting.roundStartedAt =
|
|
254
|
+
meeting.roundStartedAt = ts();
|
|
255
255
|
log('info', `Meeting ${meetingId}: all debate responses in — advancing to conclusion`);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
@@ -263,7 +263,7 @@ function addMeetingNote(meetingId, note) {
|
|
|
263
263
|
const meeting = getMeeting(meetingId);
|
|
264
264
|
if (!meeting) return null;
|
|
265
265
|
meeting.humanNotes.push(note);
|
|
266
|
-
meeting.transcript.push({ round: meeting.round, agent: 'human', type: 'note', content: note, at:
|
|
266
|
+
meeting.transcript.push({ round: meeting.round, agent: 'human', type: 'note', content: note, at: ts() });
|
|
267
267
|
saveMeeting(meeting);
|
|
268
268
|
return meeting;
|
|
269
269
|
}
|
|
@@ -279,7 +279,7 @@ function _killMeetingDispatches(meetingId) {
|
|
|
279
279
|
dp.active = (dp.active || []).filter(d => d.meta?.meetingId !== meetingId);
|
|
280
280
|
dp.completed = dp.completed || [];
|
|
281
281
|
for (const d of toKill) {
|
|
282
|
-
dp.completed.push({ ...d, result: DISPATCH_RESULT.ERROR, reason: 'Meeting ended/advanced by human', completed_at:
|
|
282
|
+
dp.completed.push({ ...d, result: DISPATCH_RESULT.ERROR, reason: 'Meeting ended/advanced by human', completed_at: ts() });
|
|
283
283
|
}
|
|
284
284
|
if (dp.completed.length > 100) dp.completed = dp.completed.slice(-100);
|
|
285
285
|
return dp;
|
|
@@ -295,9 +295,9 @@ function advanceMeetingRound(meetingId) {
|
|
|
295
295
|
_killMeetingDispatches(meetingId);
|
|
296
296
|
if (meeting.status === 'investigating') { meeting.status = 'debating'; meeting.round = 2; }
|
|
297
297
|
else if (meeting.status === 'debating') { meeting.status = 'concluding'; meeting.round = 3; }
|
|
298
|
-
else if (meeting.status === 'concluding') { meeting.status = 'completed'; meeting.completedAt =
|
|
298
|
+
else if (meeting.status === 'concluding') { meeting.status = 'completed'; meeting.completedAt = ts(); }
|
|
299
299
|
else return meeting; // no change
|
|
300
|
-
meeting.roundStartedAt =
|
|
300
|
+
meeting.roundStartedAt = ts();
|
|
301
301
|
saveMeeting(meeting);
|
|
302
302
|
return meeting;
|
|
303
303
|
}
|
|
@@ -307,7 +307,7 @@ function endMeeting(meetingId) {
|
|
|
307
307
|
if (!meeting) return null;
|
|
308
308
|
_killMeetingDispatches(meetingId);
|
|
309
309
|
meeting.status = 'completed';
|
|
310
|
-
meeting.completedAt =
|
|
310
|
+
meeting.completedAt = ts();
|
|
311
311
|
saveMeeting(meeting);
|
|
312
312
|
return meeting;
|
|
313
313
|
}
|
|
@@ -316,7 +316,7 @@ function archiveMeeting(id) {
|
|
|
316
316
|
const meeting = getMeeting(id);
|
|
317
317
|
if (!meeting) return null;
|
|
318
318
|
meeting.status = 'archived';
|
|
319
|
-
meeting.archivedAt =
|
|
319
|
+
meeting.archivedAt = ts();
|
|
320
320
|
saveMeeting(meeting);
|
|
321
321
|
return meeting;
|
|
322
322
|
}
|
|
@@ -364,17 +364,17 @@ function checkMeetingTimeouts(config) {
|
|
|
364
364
|
|
|
365
365
|
if (meeting.status === 'investigating') {
|
|
366
366
|
log('warn', `Meeting ${meeting.id}: round 1 timed out after ${Math.round(elapsed / 60000)}min — ${respondedCount}/${totalCount} responded, advancing to debate`);
|
|
367
|
-
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 1 timed out — ${respondedCount}/${totalCount} findings received`, at:
|
|
367
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 1 timed out — ${respondedCount}/${totalCount} findings received`, at: ts() });
|
|
368
368
|
meeting.status = 'debating';
|
|
369
369
|
meeting.round = 2;
|
|
370
|
-
meeting.roundStartedAt =
|
|
370
|
+
meeting.roundStartedAt = ts();
|
|
371
371
|
saveMeeting(meeting);
|
|
372
372
|
} else if (meeting.status === 'debating') {
|
|
373
373
|
log('warn', `Meeting ${meeting.id}: round 2 timed out after ${Math.round(elapsed / 60000)}min — ${respondedCount}/${totalCount} responded, advancing to conclusion`);
|
|
374
|
-
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 2 timed out — ${respondedCount}/${totalCount} debate responses received`, at:
|
|
374
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 2 timed out — ${respondedCount}/${totalCount} debate responses received`, at: ts() });
|
|
375
375
|
meeting.status = 'concluding';
|
|
376
376
|
meeting.round = 3;
|
|
377
|
-
meeting.roundStartedAt =
|
|
377
|
+
meeting.roundStartedAt = ts();
|
|
378
378
|
saveMeeting(meeting);
|
|
379
379
|
} else if (meeting.status === 'concluding') {
|
|
380
380
|
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — auto-summarizing`);
|
|
@@ -386,10 +386,10 @@ function checkMeetingTimeouts(config) {
|
|
|
386
386
|
`**${(config.agents || {})[agent]?.name || agent}**: ${(d.content || '').slice(0, 200)}`
|
|
387
387
|
).join('\n');
|
|
388
388
|
const autoConclusion = `*Auto-generated — conclusion round timed out.*\n\n## Key Findings\n${findingsSummary || '(none)'}\n\n## Debate Summary\n${debateSummary || '(none)'}`;
|
|
389
|
-
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt:
|
|
390
|
-
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at:
|
|
389
|
+
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
390
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
391
391
|
meeting.status = 'completed';
|
|
392
|
-
meeting.completedAt =
|
|
392
|
+
meeting.completedAt = ts();
|
|
393
393
|
|
|
394
394
|
// Write transcript to inbox (same as normal conclusion path)
|
|
395
395
|
try {
|
package/engine/playbook.js
CHANGED
|
@@ -9,7 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
|
|
12
|
-
const { safeJson, safeRead, getProjects, log, dateStamp, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
12
|
+
const { safeJson, safeRead, getProjects, log, ts, dateStamp, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
13
13
|
const { getConfig, getDispatch, getNotes, getAgentCharter, getPrs, AGENTS_DIR } = queries;
|
|
14
14
|
|
|
15
15
|
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
@@ -133,7 +133,7 @@ function resolveTaskContext(item, config) {
|
|
|
133
133
|
try {
|
|
134
134
|
const plans = fs.readdirSync(path.join(MINIONS_DIR, 'plans')).filter(f => f.endsWith('.md') || f.endsWith('.json'));
|
|
135
135
|
// Check work-items to find which plan file this agent created
|
|
136
|
-
const workItems =
|
|
136
|
+
const workItems = queries.getWorkItems();
|
|
137
137
|
const agentPlanItems = workItems.filter(w =>
|
|
138
138
|
w.type === WORK_TYPE.PLAN && w.dispatched_to === agent.id && w.status === WI_STATUS.DONE && w._planFileName
|
|
139
139
|
).sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
|
|
@@ -241,7 +241,7 @@ function renderPlaybook(type, vars) {
|
|
|
241
241
|
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`;
|
|
242
242
|
|
|
243
243
|
// Inject learnings requirement
|
|
244
|
-
const timeStamp =
|
|
244
|
+
const timeStamp = ts().slice(11, 16).replace(':', '');
|
|
245
245
|
const inboxSlug = [vars.agent_id || 'agent', vars.task_id || '', dateStamp(), timeStamp].filter(Boolean).join('-');
|
|
246
246
|
content += `\n\n---\n\n## REQUIRED: Write Learnings\n\n`;
|
|
247
247
|
content += `After completing your task, write **one** findings file to:\n`;
|
package/engine/scheduler.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./shared');
|
|
27
|
-
const { safeJson, safeWrite, mutateJsonFileLocked, WI_STATUS } = shared;
|
|
27
|
+
const { safeJson, safeWrite, mutateJsonFileLocked, ts, WI_STATUS } = shared;
|
|
28
28
|
|
|
29
29
|
const SCHEDULE_RUNS_PATH = path.join(__dirname, 'schedule-runs.json');
|
|
30
30
|
|
|
@@ -126,7 +126,7 @@ function discoverScheduledWork(config) {
|
|
|
126
126
|
priority: sched.priority || 'medium',
|
|
127
127
|
description: sched.description || sched.title,
|
|
128
128
|
status: WI_STATUS.PENDING,
|
|
129
|
-
created:
|
|
129
|
+
created: ts(),
|
|
130
130
|
createdBy: 'scheduler',
|
|
131
131
|
agent: sched.agent || null,
|
|
132
132
|
project: sched.project || null,
|
|
@@ -134,7 +134,7 @@ function discoverScheduledWork(config) {
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
// Record run time inside the lock
|
|
137
|
-
runs[sched.id] =
|
|
137
|
+
runs[sched.id] = ts();
|
|
138
138
|
}
|
|
139
139
|
}, { defaultValue: {} });
|
|
140
140
|
|
package/engine/shared.js
CHANGED
|
@@ -738,6 +738,34 @@ function killImmediate(proc) {
|
|
|
738
738
|
}
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
// ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Atomic read-modify-write for work-items JSON files.
|
|
745
|
+
* Wraps mutateJsonFileLocked with defaultValue of [].
|
|
746
|
+
* @param {string} filePath - Path to the work-items JSON file
|
|
747
|
+
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
748
|
+
*/
|
|
749
|
+
function mutateWorkItems(filePath, mutator) {
|
|
750
|
+
return mutateJsonFileLocked(filePath, (data) => {
|
|
751
|
+
if (!Array.isArray(data)) data = [];
|
|
752
|
+
return mutator(data) || data;
|
|
753
|
+
}, { defaultValue: [] });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Atomic read-modify-write for pull-requests JSON files.
|
|
758
|
+
* Wraps mutateJsonFileLocked with defaultValue of [].
|
|
759
|
+
* @param {string} filePath - Path to the pull-requests JSON file
|
|
760
|
+
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
761
|
+
*/
|
|
762
|
+
function mutatePullRequests(filePath, mutator) {
|
|
763
|
+
return mutateJsonFileLocked(filePath, (data) => {
|
|
764
|
+
if (!Array.isArray(data)) data = [];
|
|
765
|
+
return mutator(data) || data;
|
|
766
|
+
}, { defaultValue: [] });
|
|
767
|
+
}
|
|
768
|
+
|
|
741
769
|
module.exports = {
|
|
742
770
|
MINIONS_DIR,
|
|
743
771
|
PR_LINKS_PATH,
|
|
@@ -753,6 +781,8 @@ module.exports = {
|
|
|
753
781
|
safeUnlink,
|
|
754
782
|
withFileLock,
|
|
755
783
|
mutateJsonFileLocked,
|
|
784
|
+
mutateWorkItems,
|
|
785
|
+
mutatePullRequests,
|
|
756
786
|
uid,
|
|
757
787
|
uniquePath,
|
|
758
788
|
writeToInbox,
|
package/engine/spawn-agent.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { exec, runFile, cleanChildEnv, killGracefully, killImmediate } = require('./shared');
|
|
11
|
+
const { exec, runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
|
|
12
12
|
|
|
13
13
|
const [,, promptFile, sysPromptFile, ...extraArgs] = process.argv;
|
|
14
14
|
|
|
@@ -83,7 +83,7 @@ if (!claudeBin) {
|
|
|
83
83
|
const tmpDir = path.join(__dirname, 'tmp');
|
|
84
84
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
85
85
|
const debugPath = path.join(tmpDir, 'spawn-debug.log');
|
|
86
|
-
fs.writeFileSync(debugPath, `spawn-agent.js at ${
|
|
86
|
+
fs.writeFileSync(debugPath, `spawn-agent.js at ${ts()}\nclaudeBin=${claudeBin || 'not found'}\nnative=${claudeIsNative}\nprompt=${promptFile}\nsysPrompt=${sysPromptFile}\nextraArgs=${extraArgs.join(' ')}\n`);
|
|
87
87
|
|
|
88
88
|
// When resuming a session, skip system prompt (it's baked into the session)
|
|
89
89
|
const isResume = extraArgs.includes('--resume');
|
|
@@ -119,7 +119,7 @@ if (_sysPromptFileSupported === null) {
|
|
|
119
119
|
? spawnSync(claudeBin, ['--help'], { encoding: 'utf8', timeout: 10000, windowsHide: true })
|
|
120
120
|
: spawnSync(process.execPath, [claudeBin, '--help'], { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
121
121
|
_sysPromptFileSupported = (testResult.stdout || '').includes('system-prompt-file');
|
|
122
|
-
try { fs.writeFileSync(capsCachePath, JSON.stringify({ claudeBin, sysPromptFile: _sysPromptFileSupported, checkedAt:
|
|
122
|
+
try { fs.writeFileSync(capsCachePath, JSON.stringify({ claudeBin, sysPromptFile: _sysPromptFileSupported, checkedAt: ts() })); } catch { /* optional */ }
|
|
123
123
|
} catch { _sysPromptFileSupported = true; /* assume supported */ }
|
|
124
124
|
}
|
|
125
125
|
if (!isResume) try {
|
package/engine.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./engine/shared');
|
|
27
|
-
const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
|
|
27
|
+
const { exec, execSilent, runFile, ts, ENGINE_DEFAULTS: DEFAULTS,
|
|
28
28
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
29
29
|
const queries = require('./engine/queries');
|
|
30
30
|
|
|
@@ -149,16 +149,8 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
149
149
|
const projects = shared.getProjects(config);
|
|
150
150
|
|
|
151
151
|
// Find work items for each dependency plan item
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const wiPath = shared.projectWorkItemsPath(p);
|
|
155
|
-
const items = safeJson(wiPath) || [];
|
|
156
|
-
for (const wi of items) {
|
|
157
|
-
if (depIds.includes(wi.id)) {
|
|
158
|
-
depWorkItems.push(wi);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
152
|
+
const allItems = queries.getWorkItems(config);
|
|
153
|
+
const depWorkItems = allItems.filter(wi => depIds.includes(wi.id));
|
|
162
154
|
|
|
163
155
|
// Find PR branches for each dependency work item
|
|
164
156
|
for (const p of projects) {
|
|
@@ -542,7 +534,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
542
534
|
if (obj.session_id) {
|
|
543
535
|
procInfo.sessionId = obj.session_id;
|
|
544
536
|
safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
545
|
-
sessionId: obj.session_id, dispatchId: id, savedAt:
|
|
537
|
+
sessionId: obj.session_id, dispatchId: id, savedAt: ts(), branch: branchName
|
|
546
538
|
});
|
|
547
539
|
break;
|
|
548
540
|
}
|
|
@@ -837,13 +829,7 @@ function areDependenciesMet(item, config) {
|
|
|
837
829
|
const projects = getProjects(config);
|
|
838
830
|
|
|
839
831
|
// Collect work items from ALL projects (dependencies can be cross-project)
|
|
840
|
-
|
|
841
|
-
for (const p of projects) {
|
|
842
|
-
try {
|
|
843
|
-
const wi = safeJson(projectWorkItemsPath(p)) || [];
|
|
844
|
-
allWorkItems = allWorkItems.concat(wi);
|
|
845
|
-
} catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
|
|
846
|
-
}
|
|
832
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
847
833
|
// PRD item statuses that count as "done" for dep resolution
|
|
848
834
|
const PRD_MET_STATUSES = DONE_STATUSES;
|
|
849
835
|
|
|
@@ -1098,7 +1084,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1098
1084
|
log('info', `Source plan ${plan.source_plan} updated — re-syncing PRD ${file}`);
|
|
1099
1085
|
autoCleanPrdWorkItems(file, config);
|
|
1100
1086
|
plan.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
|
|
1101
|
-
plan.lastSyncedFromPlan =
|
|
1087
|
+
plan.lastSyncedFromPlan = ts();
|
|
1102
1088
|
|
|
1103
1089
|
// Handle PRD based on current status
|
|
1104
1090
|
const prdStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
|
|
@@ -1170,7 +1156,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1170
1156
|
if (planStatus === 'awaiting-approval') {
|
|
1171
1157
|
if (config.engine?.autoApprovePlans) {
|
|
1172
1158
|
plan.status = 'approved';
|
|
1173
|
-
plan.approvedAt =
|
|
1159
|
+
plan.approvedAt = ts();
|
|
1174
1160
|
plan.approvedBy = 'auto-mode';
|
|
1175
1161
|
safeWrite(path.join(PRD_DIR, file), plan);
|
|
1176
1162
|
log('info', `Auto-approved plan: ${file}`);
|
|
@@ -1195,13 +1181,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
1195
1181
|
const statusFilter = ['missing', 'planned'];
|
|
1196
1182
|
// Also materialize in-pr/done items that never got a work item (race with PR status sync)
|
|
1197
1183
|
const allExistingWiIds = new Set();
|
|
1198
|
-
for (const
|
|
1199
|
-
for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
|
|
1200
|
-
if (w.id) allExistingWiIds.add(w.id);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
// Also check central work-items.json
|
|
1204
|
-
for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
|
|
1184
|
+
for (const w of queries.getWorkItems()) {
|
|
1205
1185
|
if (w.id) allExistingWiIds.add(w.id);
|
|
1206
1186
|
}
|
|
1207
1187
|
const items = plan.missing_features.filter(f =>
|
|
@@ -1452,7 +1432,7 @@ function discoverFromPrs(config, project) {
|
|
|
1452
1432
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) {
|
|
1453
1433
|
// Coalesce: save feedback for next dispatch
|
|
1454
1434
|
if (pr.humanFeedback?.feedbackContent) {
|
|
1455
|
-
setCooldownWithContext(key, { feedbackContent: pr.humanFeedback.feedbackContent, timestamp:
|
|
1435
|
+
setCooldownWithContext(key, { feedbackContent: pr.humanFeedback.feedbackContent, timestamp: ts() });
|
|
1456
1436
|
}
|
|
1457
1437
|
continue;
|
|
1458
1438
|
}
|
|
@@ -2307,10 +2287,9 @@ function discoverWork(config) {
|
|
|
2307
2287
|
}
|
|
2308
2288
|
|
|
2309
2289
|
// Gate reviews and fixes: do not dispatch until all implement items are complete
|
|
2310
|
-
const hasIncompleteImplements =
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
});
|
|
2290
|
+
const hasIncompleteImplements = queries.getWorkItems(config).some(i =>
|
|
2291
|
+
['queued', 'pending', 'dispatched'].includes(i.status) && (i.type || '').startsWith('implement')
|
|
2292
|
+
);
|
|
2314
2293
|
if (hasIncompleteImplements) {
|
|
2315
2294
|
if (allReviews.length > 0) {
|
|
2316
2295
|
log('info', `Gating ${allReviews.length} reviews — implement items still in progress`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.527",
|
|
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"
|