@yemi33/minions 0.1.144 → 0.1.146
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 +17 -1
- package/dashboard/js/render-other.js +24 -1
- package/dashboard/js/render-work-items.js +5 -5
- package/dashboard/pages/engine.html +4 -0
- package/dashboard/styles.css +2 -0
- package/engine/lifecycle.js +191 -0
- package/engine/meeting.js +6 -2
- package/engine/playbook.js +32 -0
- package/engine/shared.js +2 -0
- package/engine.js +95 -7
- package/package.json +1 -1
- package/playbooks/evaluate.md +114 -0
- package/playbooks/fix.md +2 -0
- package/playbooks/implement.md +2 -0
- package/routing.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.146 (2026-04-01)
|
|
4
4
|
|
|
5
5
|
### Engine
|
|
6
|
+
- engine.js
|
|
6
7
|
- engine/ado.js
|
|
7
8
|
- engine/cleanup.js
|
|
8
9
|
- engine/cooldown.js
|
|
9
10
|
- engine/github.js
|
|
11
|
+
- engine/lifecycle.js
|
|
10
12
|
- engine/meeting.js
|
|
11
13
|
- engine/playbook.js
|
|
12
14
|
- engine/routing.js
|
|
15
|
+
- engine/shared.js
|
|
13
16
|
- engine/timeout.js
|
|
14
17
|
|
|
18
|
+
### Dashboard
|
|
19
|
+
- dashboard/js/render-other.js
|
|
20
|
+
- dashboard/js/render-work-items.js
|
|
21
|
+
- dashboard/pages/engine.html
|
|
22
|
+
- dashboard/styles.css
|
|
23
|
+
|
|
24
|
+
### Playbooks
|
|
25
|
+
- evaluate.md
|
|
26
|
+
- fix.md
|
|
27
|
+
- implement.md
|
|
28
|
+
|
|
15
29
|
### Other
|
|
30
|
+
- CLAUDE.md
|
|
31
|
+
- routing.md
|
|
16
32
|
- test/unit.test.js
|
|
17
33
|
|
|
18
34
|
## 0.1.143 (2026-04-01)
|
|
@@ -44,6 +44,7 @@ function renderMetrics(metrics) {
|
|
|
44
44
|
if (agents.length === 0) {
|
|
45
45
|
el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
|
|
46
46
|
renderTokenUsage(metrics);
|
|
47
|
+
renderContextPressure(metrics);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
|
|
@@ -64,6 +65,7 @@ function renderMetrics(metrics) {
|
|
|
64
65
|
html += '</tbody></table>';
|
|
65
66
|
el.innerHTML = html;
|
|
66
67
|
renderTokenUsage(metrics);
|
|
68
|
+
renderContextPressure(metrics);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function renderTokenUsage(metrics) {
|
|
@@ -183,4 +185,25 @@ function renderTokenUsage(metrics) {
|
|
|
183
185
|
el.innerHTML = html;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
function renderContextPressure(metrics) {
|
|
189
|
+
const el = document.getElementById('context-pressure-content');
|
|
190
|
+
if (!el) return;
|
|
191
|
+
const cp = metrics._contextPressure;
|
|
192
|
+
if (!cp || !cp.dispatches) {
|
|
193
|
+
el.innerHTML = '<p class="empty">No context pressure data yet. Data appears after agents complete tasks.</p>';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const avgTurns = (cp.totalTurns / cp.dispatches).toFixed(1);
|
|
197
|
+
const turnLimitPct = ((cp.turnLimitHits / cp.dispatches) * 100).toFixed(1);
|
|
198
|
+
const pctColor = parseFloat(turnLimitPct) > 20 ? 'var(--red)' : parseFloat(turnLimitPct) > 5 ? 'var(--yellow, orange)' : 'var(--green)';
|
|
199
|
+
|
|
200
|
+
let html = '<div class="token-tiles">';
|
|
201
|
+
html += '<div class="token-tile"><div class="token-tile-label">Avg Turns</div><div class="token-tile-value">' + avgTurns + '</div><div class="token-tile-sub">per dispatch</div></div>';
|
|
202
|
+
html += '<div class="token-tile"><div class="token-tile-label">Max Turns</div><div class="token-tile-value">' + cp.maxTurns + '</div></div>';
|
|
203
|
+
html += '<div class="token-tile"><div class="token-tile-label">Hit Turn Limit</div><div class="token-tile-value" style="color:' + pctColor + '">' + turnLimitPct + '%</div><div class="token-tile-sub">' + cp.turnLimitHits + ' of ' + cp.dispatches + ' dispatches</div></div>';
|
|
204
|
+
html += '<div class="token-tile"><div class="token-tile-label">Total Dispatches</div><div class="token-tile-value">' + cp.dispatches + '</div></div>';
|
|
205
|
+
html += '</div>';
|
|
206
|
+
el.innerHTML = html;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
window.MinionsOther = { renderProjects, renderMcpServers, renderMetrics, renderTokenUsage, renderContextPressure };
|
|
@@ -6,7 +6,7 @@ const WI_PER_PAGE = 20;
|
|
|
6
6
|
|
|
7
7
|
function wiRow(item) {
|
|
8
8
|
const statusBadge = (s) => {
|
|
9
|
-
const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
9
|
+
const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
10
10
|
return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
|
|
11
11
|
};
|
|
12
12
|
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
|
|
@@ -38,9 +38,9 @@ function wiRow(item) {
|
|
|
38
38
|
(item.acceptanceCriteria && item.acceptanceCriteria.length ? '<span title="' + item.acceptanceCriteria.length + ' acceptance criteria">☑' + item.acceptanceCriteria.length + '</span>' : '') +
|
|
39
39
|
'</td>' +
|
|
40
40
|
'<td style="white-space:nowrap">' +
|
|
41
|
-
((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">✎</button>' : '') +
|
|
42
|
-
((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
43
|
-
((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Give feedback">👍👎</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '👍' : '👎') + '</span> ' : '')) +
|
|
41
|
+
((item.status === 'pending' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">✎</button>' : '') +
|
|
42
|
+
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
43
|
+
((item.status === 'done' || item.status === 'failed' || item.status === 'needs-human-review') && !item._humanFeedback ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Give feedback">👍👎</button>' : (item._humanFeedback ? '<span style="font-size:9px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '👍' : '👎') + '</span> ' : '')) +
|
|
44
44
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
45
45
|
'</td>' +
|
|
46
46
|
'</tr>';
|
|
@@ -49,7 +49,7 @@ function wiRow(item) {
|
|
|
49
49
|
function renderWorkItems(items) {
|
|
50
50
|
items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
|
|
51
51
|
// Sort: active/dispatched first, then by most recent activity
|
|
52
|
-
const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
|
|
52
|
+
const statusOrder = { dispatched: 0, pending: 1, queued: 1, 'needs-human-review': 2, failed: 2, done: 3 };
|
|
53
53
|
items.sort((a, b) => {
|
|
54
54
|
const sa = statusOrder[a.status] ?? 2, sb = statusOrder[b.status] ?? 2;
|
|
55
55
|
if (sa !== sb) return sa - sb;
|
|
@@ -10,3 +10,7 @@
|
|
|
10
10
|
<h2>Token Usage</h2>
|
|
11
11
|
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
12
12
|
</section>
|
|
13
|
+
<section>
|
|
14
|
+
<h2>Context Pressure</h2>
|
|
15
|
+
<div id="context-pressure-content"><p class="empty">No context pressure data yet. Data appears after agents complete tasks.</p></div>
|
|
16
|
+
</section>
|
package/dashboard/styles.css
CHANGED
|
@@ -186,6 +186,7 @@
|
|
|
186
186
|
.prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
|
|
187
187
|
@keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
|
|
188
188
|
.prd-item-row.st-failed { border-left-color: var(--red); }
|
|
189
|
+
.prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
|
|
189
190
|
.prd-item-row.st-paused { border-left-color: var(--muted); opacity: 0.5; }
|
|
190
191
|
.prd-item-id { font-family: Consolas, monospace; color: var(--muted); min-width: 36px; font-size: 0.9em; }
|
|
191
192
|
.prd-item-name { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -232,6 +233,7 @@
|
|
|
232
233
|
.pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
|
233
234
|
.pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
234
235
|
.pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
236
|
+
.pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
|
|
235
237
|
.pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
|
|
236
238
|
.pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
|
237
239
|
.pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
package/engine/lifecycle.js
CHANGED
|
@@ -932,6 +932,30 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
932
932
|
if (wrote) log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
function recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit) {
|
|
936
|
+
const itemId = meta.item?.id;
|
|
937
|
+
if (!itemId) return;
|
|
938
|
+
|
|
939
|
+
let wiPath;
|
|
940
|
+
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
941
|
+
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
942
|
+
} else if (meta.source === 'work-item' && meta.project?.name) {
|
|
943
|
+
wiPath = path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
|
|
944
|
+
}
|
|
945
|
+
if (!wiPath) return;
|
|
946
|
+
|
|
947
|
+
shared.mutateJsonFileLocked(wiPath, (items) => {
|
|
948
|
+
if (!Array.isArray(items)) return items;
|
|
949
|
+
const target = items.find(i => i.id === itemId);
|
|
950
|
+
if (target) {
|
|
951
|
+
target._turnCount = turnCount;
|
|
952
|
+
target._outputLogSizeBytes = outputLogSizeBytes;
|
|
953
|
+
target._hitTurnLimit = hitTurnLimit;
|
|
954
|
+
}
|
|
955
|
+
return items;
|
|
956
|
+
}, { defaultValue: [] });
|
|
957
|
+
}
|
|
958
|
+
|
|
935
959
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
|
|
936
960
|
|
|
937
961
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
@@ -977,6 +1001,20 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
977
1001
|
for (const day of Object.keys(metrics._daily)) {
|
|
978
1002
|
if (day < cutoffStr) delete metrics._daily[day];
|
|
979
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
// Update contextPressure aggregate
|
|
1006
|
+
if (taskUsage && taskUsage.numTurns > 0) {
|
|
1007
|
+
if (!metrics._contextPressure) metrics._contextPressure = { totalTurns: 0, dispatches: 0, maxTurns: 0, turnLimitHits: 0 };
|
|
1008
|
+
const cp = metrics._contextPressure;
|
|
1009
|
+
cp.totalTurns += taskUsage.numTurns;
|
|
1010
|
+
cp.dispatches++;
|
|
1011
|
+
if (taskUsage.numTurns > cp.maxTurns) cp.maxTurns = taskUsage.numTurns;
|
|
1012
|
+
// Check if this dispatch hit the turn limit
|
|
1013
|
+
const engineConfig = require('./queries').getConfig()?.engine || {};
|
|
1014
|
+
const turnLimit = engineConfig.maxTurns || 100;
|
|
1015
|
+
if (taskUsage.numTurns >= turnLimit) cp.turnLimitHits++;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
980
1018
|
shared.safeWrite(metricsPath, metrics);
|
|
981
1019
|
}
|
|
982
1020
|
|
|
@@ -987,6 +1025,40 @@ function parseAgentOutput(stdout) {
|
|
|
987
1025
|
return { resultSummary: text, taskUsage: usage, sessionId, model };
|
|
988
1026
|
}
|
|
989
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Resolve work-items.json path from dispatch meta.
|
|
1030
|
+
* Central items → MINIONS_DIR/work-items.json; project items → projects/<name>/work-items.json.
|
|
1031
|
+
*/
|
|
1032
|
+
function resolveWiPath(meta) {
|
|
1033
|
+
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
1034
|
+
return path.join(MINIONS_DIR, 'work-items.json');
|
|
1035
|
+
}
|
|
1036
|
+
if (meta.project?.name) {
|
|
1037
|
+
return path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Parse structured eval verdict from evaluate agent output.
|
|
1044
|
+
* Looks for a JSON block with { pass, build, tests, criteria_met, criteria_failed, feedback }.
|
|
1045
|
+
* Returns parsed object or null if not found.
|
|
1046
|
+
*/
|
|
1047
|
+
function parseEvalVerdict(text) {
|
|
1048
|
+
if (!text) return null;
|
|
1049
|
+
// Look for JSON fenced block first, then bare JSON
|
|
1050
|
+
const fenced = text.match(/```(?:json)?\s*\n(\{[\s\S]*?"pass"\s*:[\s\S]*?\})\s*\n```/);
|
|
1051
|
+
if (fenced) {
|
|
1052
|
+
try { return JSON.parse(fenced[1]); } catch { /* fall through */ }
|
|
1053
|
+
}
|
|
1054
|
+
// Try bare JSON with "pass" key
|
|
1055
|
+
const bare = text.match(/(\{[\s\S]*?"pass"\s*:[\s\S]*?\})/);
|
|
1056
|
+
if (bare) {
|
|
1057
|
+
try { return JSON.parse(bare[1]); } catch { /* ignore */ }
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
990
1062
|
/**
|
|
991
1063
|
* Handle decomposition result — parse sub-items from agent output and create child work items.
|
|
992
1064
|
* Called from runPostCompletionHooks when type === 'decompose'.
|
|
@@ -1089,7 +1161,124 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1089
1161
|
// If decomposition produced nothing, fall through to mark parent as done
|
|
1090
1162
|
}
|
|
1091
1163
|
|
|
1164
|
+
// Record context utilization metrics on the work item
|
|
1165
|
+
if (meta?.item?.id) {
|
|
1166
|
+
try {
|
|
1167
|
+
const engineConfig = (config.engine || {});
|
|
1168
|
+
const turnLimit = engineConfig.maxTurns || 100;
|
|
1169
|
+
const turnCount = taskUsage?.numTurns || 0;
|
|
1170
|
+
const hitTurnLimit = turnCount >= turnLimit;
|
|
1171
|
+
let outputLogSizeBytes = 0;
|
|
1172
|
+
try {
|
|
1173
|
+
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
1174
|
+
const stat = fs.statSync(liveLogPath);
|
|
1175
|
+
outputLogSizeBytes = stat.size;
|
|
1176
|
+
} catch { /* file may not exist */ }
|
|
1177
|
+
recordContextPressureOnWorkItem(meta, turnCount, outputLogSizeBytes, hitTurnLimit);
|
|
1178
|
+
} catch (err) { log('warn', `Context pressure record: ${err.message}`); }
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1092
1181
|
if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
|
|
1182
|
+
|
|
1183
|
+
// Auto-dispatch evaluate work item after implement completes successfully
|
|
1184
|
+
if (isSuccess && !skipDoneStatus && type === 'implement' && meta?.item?.id) {
|
|
1185
|
+
const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
|
|
1186
|
+
if (evalLoop) {
|
|
1187
|
+
try {
|
|
1188
|
+
const wiPath = resolveWiPath(meta);
|
|
1189
|
+
if (wiPath) {
|
|
1190
|
+
const items = safeJson(wiPath) || [];
|
|
1191
|
+
// Dedup: skip if an evaluate item already exists for this parent
|
|
1192
|
+
const existing = items.find(i => i._evalParentId === meta.item.id && i.type === 'evaluate');
|
|
1193
|
+
if (existing) {
|
|
1194
|
+
log('info', `Eval loop: evaluate item ${existing.id} already exists for ${meta.item.id}, skipping`);
|
|
1195
|
+
} else {
|
|
1196
|
+
const parentItem = items.find(i => i.id === meta.item.id);
|
|
1197
|
+
const evalItem = {
|
|
1198
|
+
id: 'W-' + shared.uid(),
|
|
1199
|
+
title: `Evaluate: ${meta.item.title || meta.item.id}`,
|
|
1200
|
+
type: 'evaluate',
|
|
1201
|
+
priority: meta.item.priority || 'high',
|
|
1202
|
+
status: 'pending',
|
|
1203
|
+
created: ts(),
|
|
1204
|
+
createdBy: 'engine:eval-loop',
|
|
1205
|
+
project: meta.project?.name || meta.item.project,
|
|
1206
|
+
branch_name: parentItem?.branch_name || meta.branch || null,
|
|
1207
|
+
pr_url: parentItem?.pr_url || null,
|
|
1208
|
+
acceptance_criteria: parentItem?.acceptance_criteria || meta.item.acceptance_criteria || null,
|
|
1209
|
+
_evalParentId: meta.item.id,
|
|
1210
|
+
};
|
|
1211
|
+
if (parentItem?.sourcePlan) evalItem.sourcePlan = parentItem.sourcePlan;
|
|
1212
|
+
// Mark parent as eval-dispatched before writing to prevent duplicates on re-run
|
|
1213
|
+
if (parentItem) parentItem._evalDispatched = true;
|
|
1214
|
+
items.push(evalItem);
|
|
1215
|
+
shared.safeWrite(wiPath, items);
|
|
1216
|
+
log('info', `Eval loop: created ${evalItem.id} for completed implement ${meta.item.id}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
log('warn', `Eval loop dispatch error: ${err.message}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Evaluate completion: parse verdict and handle eval→fix iteration loop
|
|
1226
|
+
if (isSuccess && type === 'evaluate' && meta?.item?._evalParentId) {
|
|
1227
|
+
try {
|
|
1228
|
+
const verdict = parseEvalVerdict(resultSummary || stdout);
|
|
1229
|
+
const evalLoop = config.engine?.evalLoop ?? shared.ENGINE_DEFAULTS.evalLoop;
|
|
1230
|
+
const maxIter = config.engine?.evalMaxIterations ?? shared.ENGINE_DEFAULTS.evalMaxIterations;
|
|
1231
|
+
|
|
1232
|
+
if (verdict && !verdict.pass && evalLoop) {
|
|
1233
|
+
const wiPath = resolveWiPath(meta);
|
|
1234
|
+
if (wiPath) {
|
|
1235
|
+
const items = safeJson(wiPath) || [];
|
|
1236
|
+
const parent = items.find(i => i.id === meta.item._evalParentId);
|
|
1237
|
+
if (parent) {
|
|
1238
|
+
const iterations = (parent._evalIterations || 0) + 1;
|
|
1239
|
+
parent._evalIterations = iterations;
|
|
1240
|
+
|
|
1241
|
+
if (iterations >= maxIter) {
|
|
1242
|
+
// Max iterations reached — escalate to human review
|
|
1243
|
+
parent.status = 'needs-human-review';
|
|
1244
|
+
parent._evalEscalatedAt = ts();
|
|
1245
|
+
shared.safeWrite(wiPath, items);
|
|
1246
|
+
log('info', `Eval loop: ${parent.id} reached ${iterations}/${maxIter} iterations — escalated to needs-human-review`);
|
|
1247
|
+
} else {
|
|
1248
|
+
// Create fix work item with evaluator feedback
|
|
1249
|
+
const fixItem = {
|
|
1250
|
+
id: 'W-' + shared.uid(),
|
|
1251
|
+
title: `Fix: ${parent.title || parent.id} (eval iteration ${iterations})`,
|
|
1252
|
+
type: 'fix',
|
|
1253
|
+
priority: parent.priority || 'high',
|
|
1254
|
+
status: 'pending',
|
|
1255
|
+
created: ts(),
|
|
1256
|
+
createdBy: 'engine:eval-loop',
|
|
1257
|
+
project: meta.project?.name || meta.item.project,
|
|
1258
|
+
branch_name: parent.branch_name || meta.item.branch_name || null,
|
|
1259
|
+
pr_url: parent.pr_url || meta.item.pr_url || null,
|
|
1260
|
+
acceptance_criteria: parent.acceptance_criteria || null,
|
|
1261
|
+
_evalParentId: parent.id,
|
|
1262
|
+
_evalFeedback: verdict.feedback || null,
|
|
1263
|
+
_evalCriteriaFailed: verdict.criteria_failed || null,
|
|
1264
|
+
};
|
|
1265
|
+
if (parent.sourcePlan) fixItem.sourcePlan = parent.sourcePlan;
|
|
1266
|
+
// Clear eval-dispatched flag so next fix→eval cycle can dispatch
|
|
1267
|
+
parent._evalDispatched = false;
|
|
1268
|
+
// Parent stays 'done' — fix item picks up from here
|
|
1269
|
+
parent.status = 'done';
|
|
1270
|
+
items.push(fixItem);
|
|
1271
|
+
shared.safeWrite(wiPath, items);
|
|
1272
|
+
log('info', `Eval loop: created fix ${fixItem.id} for failed eval on ${parent.id} (iteration ${iterations}/${maxIter})`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
log('warn', `Eval verdict processing error: ${err.message}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1093
1282
|
if (!isSuccess && meta?.item?.id) {
|
|
1094
1283
|
// Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
|
|
1095
1284
|
let retries = (meta.item._retryCount || 0);
|
|
@@ -1289,7 +1478,9 @@ module.exports = {
|
|
|
1289
1478
|
updateAgentHistory,
|
|
1290
1479
|
createReviewFeedbackForAuthor,
|
|
1291
1480
|
updateMetrics,
|
|
1481
|
+
recordContextPressureOnWorkItem,
|
|
1292
1482
|
parseAgentOutput,
|
|
1483
|
+
parseEvalVerdict,
|
|
1293
1484
|
runPostCompletionHooks,
|
|
1294
1485
|
syncPrdFromPrs,
|
|
1295
1486
|
};
|
package/engine/meeting.js
CHANGED
|
@@ -78,8 +78,12 @@ function discoverMeetingWork(config) {
|
|
|
78
78
|
const agents = config.agents || {};
|
|
79
79
|
|
|
80
80
|
if (roundName === 'concluding') {
|
|
81
|
-
//
|
|
82
|
-
const
|
|
81
|
+
// Pick the first non-busy participant as concluder (fallback to any participant)
|
|
82
|
+
const busyAgents = new Set(
|
|
83
|
+
(dispatch.active || []).map(d => d.agent).filter(Boolean)
|
|
84
|
+
);
|
|
85
|
+
const concluder = meeting.participants.find(p => !busyAgents.has(p))
|
|
86
|
+
|| meeting.participants[0];
|
|
83
87
|
if (!concluder) continue;
|
|
84
88
|
const key = `meeting-${meeting.id}-r${round}-${concluder}`;
|
|
85
89
|
if (activeKeys.has(key)) continue;
|
package/engine/playbook.js
CHANGED
|
@@ -206,9 +206,25 @@ function resolveTaskContext(item, config) {
|
|
|
206
206
|
return resolved;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// ─── Critical Variable Definitions ─────────────────────────────────────────
|
|
210
|
+
// Variables that MUST resolve to non-empty values for dispatch to proceed.
|
|
211
|
+
// If any critical variable is empty or unresolved, renderPlaybook returns null.
|
|
212
|
+
const CRITICAL_VARS = {
|
|
213
|
+
'implement': ['task_description', 'branch_name'],
|
|
214
|
+
'implement-shared': ['task_description', 'branch_name'],
|
|
215
|
+
'fix': ['task_description', 'branch_name'],
|
|
216
|
+
'work-item': ['task_description'],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Module-level error state — callers check via getLastRenderError() after null return
|
|
220
|
+
let _lastRenderError = null;
|
|
221
|
+
|
|
222
|
+
function getLastRenderError() { return _lastRenderError; }
|
|
223
|
+
|
|
209
224
|
// ─── Playbook Renderer ──────────────────────────────────────────────────────
|
|
210
225
|
|
|
211
226
|
function renderPlaybook(type, vars) {
|
|
227
|
+
_lastRenderError = null;
|
|
212
228
|
const pbPath = path.join(PLAYBOOKS_DIR, `${type}.md`);
|
|
213
229
|
let content;
|
|
214
230
|
try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
|
|
@@ -299,6 +315,20 @@ function renderPlaybook(type, vars) {
|
|
|
299
315
|
log('warn', `Playbook "${type}": unresolved template variables: ${unresolved.join(', ')}`);
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
// Block dispatch if critical variables are empty or unresolved
|
|
319
|
+
const criticalVars = CRITICAL_VARS[type] || [];
|
|
320
|
+
if (criticalVars.length > 0) {
|
|
321
|
+
const emptySet = new Set(emptyVars);
|
|
322
|
+
const unresolvedSet = new Set(unresolved);
|
|
323
|
+
const criticalMissing = criticalVars.filter(v => emptySet.has(v) || unresolvedSet.has(v));
|
|
324
|
+
if (criticalMissing.length > 0) {
|
|
325
|
+
const msg = `Playbook "${type}": critical variables empty or unresolved: ${criticalMissing.join(', ')} — blocking dispatch`;
|
|
326
|
+
log('warn', msg);
|
|
327
|
+
_lastRenderError = { reason: 'critical_vars_missing', vars: criticalMissing, message: msg };
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
302
332
|
return content;
|
|
303
333
|
}
|
|
304
334
|
|
|
@@ -470,6 +500,8 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
470
500
|
|
|
471
501
|
module.exports = {
|
|
472
502
|
renderPlaybook,
|
|
503
|
+
getLastRenderError,
|
|
504
|
+
CRITICAL_VARS,
|
|
473
505
|
buildSystemPrompt,
|
|
474
506
|
buildAgentContext,
|
|
475
507
|
selectPlaybook,
|
package/engine/shared.js
CHANGED
|
@@ -356,6 +356,8 @@ const ENGINE_DEFAULTS = {
|
|
|
356
356
|
autoApprovePlans: false, // auto-approve PRDs without waiting for human approval
|
|
357
357
|
autoReview: true, // auto-dispatch review agents for new PRs (disable for manual review workflow)
|
|
358
358
|
meetingRoundTimeout: 600000, // 10min per meeting round before auto-advance
|
|
359
|
+
evalLoop: true, // enable evaluate→fix loop after implementation completes
|
|
360
|
+
evalMaxIterations: 3, // max evaluate→fix cycles before escalating to human
|
|
359
361
|
};
|
|
360
362
|
|
|
361
363
|
const DEFAULT_AGENTS = {
|
package/engine.js
CHANGED
|
@@ -120,7 +120,7 @@ const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
|
120
120
|
|
|
121
121
|
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
122
122
|
|
|
123
|
-
const { renderPlaybook, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
123
|
+
const { renderPlaybook, getLastRenderError, buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
124
124
|
buildBaseVars, buildPrDispatch, resolveTaskContext,
|
|
125
125
|
getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
|
|
126
126
|
|
|
@@ -1474,6 +1474,38 @@ function discoverFromWorkItems(config, project) {
|
|
|
1474
1474
|
const ac = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
|
|
1475
1475
|
vars.acceptance_criteria = ac ? '## Acceptance Criteria\n\n' + ac : '';
|
|
1476
1476
|
|
|
1477
|
+
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1478
|
+
vars.checkpoint_context = '';
|
|
1479
|
+
try {
|
|
1480
|
+
const wtPath = vars.worktree_path || root;
|
|
1481
|
+
const cpPath = path.join(wtPath, 'checkpoint.json');
|
|
1482
|
+
if (fs.existsSync(cpPath)) {
|
|
1483
|
+
const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
|
|
1484
|
+
const cpCount = (item._checkpointCount || 0) + 1;
|
|
1485
|
+
if (cpCount > 3) {
|
|
1486
|
+
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
1487
|
+
item.status = 'needs-human-review';
|
|
1488
|
+
item._checkpointCount = cpCount;
|
|
1489
|
+
needsWrite = true;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
item._checkpointCount = cpCount;
|
|
1493
|
+
needsWrite = true;
|
|
1494
|
+
const cpSummary = [
|
|
1495
|
+
`## Checkpoint (Resume #${cpCount}/3)`,
|
|
1496
|
+
'',
|
|
1497
|
+
'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
|
|
1498
|
+
'',
|
|
1499
|
+
cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
|
|
1500
|
+
cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
|
|
1501
|
+
cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
|
|
1502
|
+
cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
|
|
1503
|
+
].filter(Boolean).join('\n');
|
|
1504
|
+
vars.checkpoint_context = cpSummary;
|
|
1505
|
+
log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
|
|
1506
|
+
}
|
|
1507
|
+
} catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
|
|
1508
|
+
|
|
1477
1509
|
// Inject ask-specific variables for the ask playbook
|
|
1478
1510
|
if (workType === 'ask') {
|
|
1479
1511
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
@@ -1493,9 +1525,17 @@ function discoverFromWorkItems(config, project) {
|
|
|
1493
1525
|
if (playbookName === 'work-item' && workType === 'review') {
|
|
1494
1526
|
log('info', `Work item ${item.id} is type "review" but has no PR — using work-item playbook`);
|
|
1495
1527
|
}
|
|
1496
|
-
const
|
|
1528
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1529
|
+
const renderError = getLastRenderError();
|
|
1530
|
+
// If critical vars are missing, block dispatch entirely — don't fall through to work-item playbook
|
|
1531
|
+
const prompt = item.prompt || rendered || (renderError ? null : (renderPlaybook('work-item', vars) || item.description));
|
|
1497
1532
|
if (!prompt) {
|
|
1498
|
-
|
|
1533
|
+
if (renderError) {
|
|
1534
|
+
log('warn', `Skipping ${item.id}: ${renderError.message}`);
|
|
1535
|
+
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; needsWrite = true; }
|
|
1536
|
+
} else {
|
|
1537
|
+
log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
|
|
1538
|
+
}
|
|
1499
1539
|
continue;
|
|
1500
1540
|
}
|
|
1501
1541
|
|
|
@@ -1782,9 +1822,16 @@ function discoverCentralWorkItems(config) {
|
|
|
1782
1822
|
}
|
|
1783
1823
|
|
|
1784
1824
|
const playbookName = selectPlaybook(workType, item);
|
|
1785
|
-
const
|
|
1825
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1826
|
+
const renderError = getLastRenderError();
|
|
1827
|
+
const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
|
|
1786
1828
|
if (!prompt) {
|
|
1787
|
-
|
|
1829
|
+
if (renderError) {
|
|
1830
|
+
log('warn', `Fan-out: ${item.id} → ${agent.id}: ${renderError.message}`);
|
|
1831
|
+
if (item._pendingReason !== 'critical_vars_missing') { item._pendingReason = 'critical_vars_missing'; }
|
|
1832
|
+
} else {
|
|
1833
|
+
log('warn', `Fan-out: playbook '${playbookName}' failed to render for ${item.id} → ${agent.id}, skipping`);
|
|
1834
|
+
}
|
|
1788
1835
|
continue;
|
|
1789
1836
|
}
|
|
1790
1837
|
|
|
@@ -1844,6 +1891,39 @@ function discoverCentralWorkItems(config) {
|
|
|
1844
1891
|
const normAc = (item.acceptanceCriteria || []).map(c => '- [ ] ' + c).join('\n');
|
|
1845
1892
|
vars.acceptance_criteria = normAc ? '## Acceptance Criteria\n\n' + normAc : '';
|
|
1846
1893
|
|
|
1894
|
+
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1895
|
+
vars.checkpoint_context = '';
|
|
1896
|
+
try {
|
|
1897
|
+
const centralBranch = item.branch || `work/${item.id}`;
|
|
1898
|
+
const centralWtPath = firstProject?.localPath
|
|
1899
|
+
? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
|
|
1900
|
+
: '';
|
|
1901
|
+
const cpPath = centralWtPath ? path.join(centralWtPath, 'checkpoint.json') : '';
|
|
1902
|
+
if (cpPath && fs.existsSync(cpPath)) {
|
|
1903
|
+
const cpData = JSON.parse(fs.readFileSync(cpPath, 'utf8'));
|
|
1904
|
+
const cpCount = (item._checkpointCount || 0) + 1;
|
|
1905
|
+
if (cpCount > 3) {
|
|
1906
|
+
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
1907
|
+
item.status = 'needs-human-review';
|
|
1908
|
+
item._checkpointCount = cpCount;
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
item._checkpointCount = cpCount;
|
|
1912
|
+
const cpSummary = [
|
|
1913
|
+
`## Checkpoint (Resume #${cpCount}/3)`,
|
|
1914
|
+
'',
|
|
1915
|
+
'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
|
|
1916
|
+
'',
|
|
1917
|
+
cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
|
|
1918
|
+
cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
|
|
1919
|
+
cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
|
|
1920
|
+
cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
|
|
1921
|
+
].filter(Boolean).join('\n');
|
|
1922
|
+
vars.checkpoint_context = cpSummary;
|
|
1923
|
+
log('info', `Injecting checkpoint context for ${item.id} (resume #${cpCount})`);
|
|
1924
|
+
}
|
|
1925
|
+
} catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
|
|
1926
|
+
|
|
1847
1927
|
// Inject plan-specific variables for the plan playbook
|
|
1848
1928
|
if (workType === 'plan') {
|
|
1849
1929
|
// Ensure plans directory exists before agent tries to write
|
|
@@ -1894,9 +1974,16 @@ function discoverCentralWorkItems(config) {
|
|
|
1894
1974
|
}
|
|
1895
1975
|
|
|
1896
1976
|
const playbookName = selectPlaybook(workType, item);
|
|
1897
|
-
const
|
|
1977
|
+
const rendered = renderPlaybook(playbookName, vars);
|
|
1978
|
+
const renderError = getLastRenderError();
|
|
1979
|
+
const prompt = rendered || (renderError ? null : renderPlaybook('work-item', vars));
|
|
1898
1980
|
if (!prompt) {
|
|
1899
|
-
|
|
1981
|
+
if (renderError) {
|
|
1982
|
+
log('warn', `Dispatch: ${item.id}: ${renderError.message}`);
|
|
1983
|
+
item._pendingReason = 'critical_vars_missing';
|
|
1984
|
+
} else {
|
|
1985
|
+
log('warn', `Dispatch: playbook '${playbookName}' failed to render for ${item.id}, resetting to pending`);
|
|
1986
|
+
}
|
|
1900
1987
|
item.status = 'pending';
|
|
1901
1988
|
continue;
|
|
1902
1989
|
}
|
|
@@ -2378,6 +2465,7 @@ module.exports = {
|
|
|
2378
2465
|
|
|
2379
2466
|
// Playbooks
|
|
2380
2467
|
renderPlaybook,
|
|
2468
|
+
getLastRenderError,
|
|
2381
2469
|
|
|
2382
2470
|
// Timeout / Steering / Idle (re-exported from engine/timeout.js)
|
|
2383
2471
|
checkTimeouts, checkSteering, checkIdleThreshold,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.146",
|
|
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,114 @@
|
|
|
1
|
+
# Evaluate: {{item_name}}
|
|
2
|
+
|
|
3
|
+
> Agent: {{agent_name}} ({{agent_role}}) | Team root: {{team_root}}
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Project: {{project_name}}
|
|
8
|
+
Repo: {{repo_name}} | Org: {{ado_org}} | ADO Project: {{ado_project}}
|
|
9
|
+
PR: {{pr_url}}
|
|
10
|
+
Work Item: {{item_id}}
|
|
11
|
+
|
|
12
|
+
## Acceptance Criteria
|
|
13
|
+
|
|
14
|
+
{{acceptance_criteria}}
|
|
15
|
+
|
|
16
|
+
## Task Description
|
|
17
|
+
|
|
18
|
+
{{task_description}}
|
|
19
|
+
|
|
20
|
+
## Your Task
|
|
21
|
+
|
|
22
|
+
You are the **Evaluator** in the Planner-Generator-Evaluator pattern. Your job is to independently verify whether the implementation in the PR branch meets the acceptance criteria. You are NOT the implementer — you are the skeptic.
|
|
23
|
+
|
|
24
|
+
**Mindset: Do not pass unless build succeeds AND all acceptance criteria are demonstrably met.** Assume the implementation is incomplete or wrong until proven otherwise. Look for edge cases, missing requirements, and silent failures.
|
|
25
|
+
|
|
26
|
+
## Step 1: Check Out the PR Branch
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd {{project_path}}
|
|
30
|
+
git fetch origin
|
|
31
|
+
git checkout {{branch_name}}
|
|
32
|
+
git pull origin {{branch_name}}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Step 2: Build
|
|
36
|
+
|
|
37
|
+
Run the project build. Check `CLAUDE.md`, `package.json`, or `README` for build instructions.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Typical:
|
|
41
|
+
npm install && npm run build
|
|
42
|
+
# Or whatever the project uses
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Record: **PASS** or **FAIL** with error output.
|
|
46
|
+
|
|
47
|
+
If the build fails, **stop here** — the verdict is `pass: false`. Include the build error in feedback.
|
|
48
|
+
|
|
49
|
+
## Step 3: Run Tests
|
|
50
|
+
|
|
51
|
+
Run the full test suite:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Record: **X passed / Y failed / Z skipped**.
|
|
58
|
+
|
|
59
|
+
If any tests fail, note which ones and whether they are related to the changes.
|
|
60
|
+
|
|
61
|
+
## Step 4: Diff Review Against Acceptance Criteria
|
|
62
|
+
|
|
63
|
+
Review the actual code changes:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git diff {{main_branch}}...{{branch_name}} --stat
|
|
67
|
+
git diff {{main_branch}}...{{branch_name}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For **each** acceptance criterion, determine:
|
|
71
|
+
- **Met**: The diff demonstrably satisfies this criterion. Cite the specific file/line.
|
|
72
|
+
- **Not met**: The diff does not satisfy this criterion, or satisfies it only partially. Explain what's missing.
|
|
73
|
+
|
|
74
|
+
Be precise. "Looks good" is not an evaluation — cite file paths and line numbers.
|
|
75
|
+
|
|
76
|
+
## Step 5: Output Structured Verdict
|
|
77
|
+
|
|
78
|
+
After completing your evaluation, output the following JSON block as your final output. This MUST be valid JSON wrapped in a `json` fenced code block:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"pass": false,
|
|
83
|
+
"build": true,
|
|
84
|
+
"tests": "42/42",
|
|
85
|
+
"criteria_met": [
|
|
86
|
+
"criterion 1 — met because X (source: path/to/file.js:42)"
|
|
87
|
+
],
|
|
88
|
+
"criteria_failed": [
|
|
89
|
+
"criterion 2 — not met because Y is missing"
|
|
90
|
+
],
|
|
91
|
+
"feedback": "Summary of what needs to change for this to pass. Be specific — file names, line numbers, what to add/fix."
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Field definitions:
|
|
96
|
+
- `pass`: `true` only if build succeeds AND **all** acceptance criteria are met. Otherwise `false`.
|
|
97
|
+
- `build`: `true` if the build completed without errors, `false` otherwise.
|
|
98
|
+
- `tests`: String in format `"passed/total"` (e.g., `"38/40"`). Use `"N/A"` if no test suite exists.
|
|
99
|
+
- `criteria_met`: Array of strings — one per criterion that IS met. Include source references.
|
|
100
|
+
- `criteria_failed`: Array of strings — one per criterion that is NOT met. Explain why.
|
|
101
|
+
- `feedback`: Actionable feedback for the implementer. Be specific about what to fix. If `pass` is `true`, use this for minor suggestions or "LGTM".
|
|
102
|
+
|
|
103
|
+
## Rules
|
|
104
|
+
|
|
105
|
+
- **No Playwright / browser testing** — this phase evaluates build, tests, and code review only.
|
|
106
|
+
- **Do NOT fix code** — only evaluate and report. You are the evaluator, not the implementer.
|
|
107
|
+
- **Do NOT rubber-stamp** — if a criterion is ambiguous, evaluate conservatively (fail it and explain).
|
|
108
|
+
- **Build failure is an automatic fail** — do not evaluate criteria if the build doesn't pass.
|
|
109
|
+
- **Every criterion must be addressed** — `criteria_met` + `criteria_failed` should cover all acceptance criteria.
|
|
110
|
+
- **Cite sources** — reference file paths and line numbers for every met/failed criterion.
|
|
111
|
+
|
|
112
|
+
{{references}}
|
|
113
|
+
|
|
114
|
+
**Note:** Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
|
package/playbooks/fix.md
CHANGED
package/playbooks/implement.md
CHANGED
|
@@ -18,6 +18,8 @@ Implement PRD item **{{item_id}}: {{item_name}}**
|
|
|
18
18
|
- Complexity: {{item_complexity}}
|
|
19
19
|
- Description: {{item_description}}
|
|
20
20
|
|
|
21
|
+
{{checkpoint_context}}
|
|
22
|
+
|
|
21
23
|
## Projects
|
|
22
24
|
|
|
23
25
|
Primary repo: **{{repo_name}}** ({{ado_org}}/{{ado_project}}) at `{{project_path}}`
|
package/routing.md
CHANGED
|
@@ -17,6 +17,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
|
|
|
17
17
|
| test | dallas | ralph |
|
|
18
18
|
| ask | ripley | rebecca |
|
|
19
19
|
| verify | dallas | ralph |
|
|
20
|
+
| evaluate | ripley | lambert |
|
|
20
21
|
| decompose | ripley | rebecca |
|
|
21
22
|
| meeting | ripley | rebecca |
|
|
22
23
|
|