@yemi33/minions 0.1.314 → 0.1.316

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 CHANGED
@@ -1,11 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.314 (2026-04-03)
3
+ ## 0.1.316 (2026-04-03)
4
4
 
5
5
  ### Fixes
6
+ - deduplicate PRs in pull-requests.json on write
6
7
  - show reviewer names in dashboard Signed Off By column
7
8
 
8
9
  ### Other
10
+ - refactor: extract status/type/result constants to shared.js
11
+ - cleanup: remove evaluate.md (re-created by agents), fix stale references
9
12
  - perf: CC message handling — debounce localStorage, cap array, batch scroll
10
13
 
11
14
  ## 0.1.312 (2026-04-03)
@@ -70,7 +70,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
70
70
  w.sourcePlan === prdFile || w.sourcePlan === mdFile ||
71
71
  (w.type === 'plan-to-prd' && (w.planFile === prdFile || w.planFile === mdFile))
72
72
  );
73
- const implementWi = wi.filter(w => w.type !== 'plan-to-prd' && w.type !== 'verify' && w.type !== 'evaluate');
73
+ const implementWi = wi.filter(w => w.type !== 'plan-to-prd' && w.type !== 'verify');
74
74
  const hasPendingPrd = wi.some(w => w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched'));
75
75
  const hasActiveWork = implementWi.some(w => w.status === 'pending' || w.status === 'dispatched');
76
76
  const allDone = implementWi.length > 0 && implementWi.every(w => w.status === 'done');
package/engine/cleanup.js CHANGED
@@ -433,7 +433,7 @@ function runCleanup(config, verbose = false) {
433
433
 
434
434
  // 6. Migrate legacy work-item statuses to canonical values
435
435
  // in-pr, implemented, complete → done (one-time correction per item)
436
- const LEGACY_DONE_STATUSES = new Set(['in-pr', 'implemented', 'complete']);
436
+ const LEGACY_DONE_STATUSES = shared.DONE_STATUSES;
437
437
  for (const project of projects) {
438
438
  try {
439
439
  const wiPath = projectWorkItemsPath(project);
@@ -10,7 +10,8 @@ const queries = require('./queries');
10
10
  const { setCooldownFailure } = require('./cooldown');
11
11
 
12
12
  const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked,
13
- getProjects, projectWorkItemsPath, log, ts, dateStamp } = shared;
13
+ getProjects, projectWorkItemsPath, log, ts, dateStamp,
14
+ WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS } = shared;
14
15
  const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
15
16
 
16
17
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -65,7 +66,7 @@ function isRetryableFailureReason(reason = '') {
65
66
 
66
67
  // ─── Complete Dispatch ───────────────────────────────────────────────────────
67
68
 
68
- function completeDispatch(id, result = 'success', reason = '', resultSummary = '', opts = {}) {
69
+ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
69
70
  const { processWorkItemFailure = true } = opts;
70
71
  let item = null;
71
72
 
@@ -98,9 +99,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
98
99
 
99
100
  // Update source work item status on failure + auto-retry with backoff
100
101
  const retryableFailure = isRetryableFailureReason(reason);
101
- if (result === 'error' && item.meta?.dispatchKey && retryableFailure) setCooldownFailure(item.meta.dispatchKey);
102
+ if (result === DISPATCH_RESULT.ERROR && item.meta?.dispatchKey && retryableFailure) setCooldownFailure(item.meta.dispatchKey);
102
103
 
103
- if (processWorkItemFailure && result === 'error' && item.meta?.item?.id) {
104
+ if (processWorkItemFailure && result === DISPATCH_RESULT.ERROR && item.meta?.item?.id) {
104
105
  let retries = (item.meta.item._retryCount || 0);
105
106
  try {
106
107
  const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
@@ -112,8 +113,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
112
113
  if (wi) retries = wi._retryCount || 0;
113
114
  }
114
115
  } catch (e) { log('warn', 'read retry count: ' + e.message); }
115
- if (retryableFailure && retries < 3) {
116
- log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/3`);
116
+ const maxRetries = ENGINE_DEFAULTS.maxRetries;
117
+ if (retryableFailure && retries < maxRetries) {
118
+ log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
117
119
  lifecycle().updateWorkItemStatus(item.meta, 'pending', '');
118
120
  // Remove this dispatch key from completed so dedupe doesn't block immediate redispatch.
119
121
  if (item.meta?.dispatchKey) {
@@ -132,9 +134,9 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
132
134
  if (wiPath) {
133
135
  const items = safeJson(wiPath) || [];
134
136
  const wi = items.find(i => i.id === item.meta.item.id);
135
- if (wi && wi.status !== 'paused') {
137
+ if (wi && wi.status !== WI_STATUS.PAUSED) {
136
138
  wi._retryCount = retries + 1;
137
- wi.status = 'pending';
139
+ wi.status = WI_STATUS.PENDING;
138
140
  wi._lastRetryReason = reason || '';
139
141
  wi._lastRetryAt = ts();
140
142
  delete wi.failReason;
@@ -148,7 +150,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
148
150
  } else {
149
151
  const finalReason = !retryableFailure
150
152
  ? `Non-retryable failure: ${reason || 'Unknown error'}`
151
- : (reason || 'Failed after 3 retries');
153
+ : (reason || `Failed after ${maxRetries} retries`);
152
154
  lifecycle().updateWorkItemStatus(item.meta, 'failed', finalReason);
153
155
  // Alert: find items blocked by this failure and write inbox note
154
156
  try {
@@ -157,11 +159,11 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
157
159
  const blockedItems = [];
158
160
  for (const p of getProjects(config)) {
159
161
  const items = safeJson(projectWorkItemsPath(p)) || [];
160
- items.filter(w => w.status === 'pending' && (w.depends_on || []).includes(failedId))
162
+ items.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
161
163
  .forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
162
164
  }
163
165
  const centralItems = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
164
- centralItems.filter(w => w.status === 'pending' && (w.depends_on || []).includes(failedId))
166
+ centralItems.filter(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(failedId))
165
167
  .forEach(w => blockedItems.push(`- \`${w.id}\` — ${w.title}`));
166
168
 
167
169
  writeInboxAlert(`failed-${failedId}`,
@@ -8,7 +8,8 @@ const path = require('path');
8
8
  const os = require('os');
9
9
  const shared = require('./shared');
10
10
  const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
11
- log, ts, dateStamp } = shared;
11
+ log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
12
+ ENGINE_DEFAULTS } = shared;
12
13
  const { trackEngineUsage } = require('./llm');
13
14
  const queries = require('./queries');
14
15
  const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
@@ -57,7 +58,7 @@ function checkPlanCompletion(meta, config) {
57
58
  const unmaterialized = [...planFeatureIds].filter(id => {
58
59
  if (workItemById[id]) return false;
59
60
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
60
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
61
+ return !(prdItem && DONE_STATUSES.has(prdItem.status));
61
62
  });
62
63
  if (unmaterialized.length > 0) {
63
64
  log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
@@ -67,17 +68,17 @@ function checkPlanCompletion(meta, config) {
67
68
  // Check 2: every feature's work item must be done (or PRD item marked done externally)
68
69
  const notDone = [...planFeatureIds].filter(id => {
69
70
  const w = workItemById[id];
70
- if (w && (w.status === 'done' || w.status === 'in-pr')) return false; // in-pr accepted for backward compat
71
+ if (w && DONE_STATUSES.has(w.status)) return false;
71
72
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
72
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
73
+ return !(prdItem && DONE_STATUSES.has(prdItem.status));
73
74
  });
74
75
  if (notDone.length > 0) {
75
76
  log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
76
77
  return;
77
78
  }
78
79
 
79
- const doneItems = planItems.filter(w => w.status === 'done' || w.status === 'in-pr');
80
- const failedItems = planItems.filter(w => w.status === 'failed');
80
+ const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
81
+ const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
81
82
 
82
83
  // 1. Mark plan as completed
83
84
  plan.status = 'completed';
@@ -490,20 +491,20 @@ function updateWorkItemStatus(meta, status, reason) {
490
491
  target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
491
492
 
492
493
  const results = Object.values(target.agentResults);
493
- const anySuccess = results.some(r => r.status === 'done');
494
+ const anySuccess = results.some(r => r.status === WI_STATUS.DONE);
494
495
  const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
495
496
  const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
496
497
  const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
497
498
 
498
499
  if (anySuccess) {
499
- target.status = 'done';
500
+ target.status = WI_STATUS.DONE;
500
501
  delete target.failReason;
501
502
  delete target.failedAt;
502
503
  target.completedAgents = Object.entries(target.agentResults)
503
- .filter(([, r]) => r.status === 'done')
504
+ .filter(([, r]) => r.status === WI_STATUS.DONE)
504
505
  .map(([a]) => a);
505
506
  } else if (allDone || timedOut) {
506
- target.status = 'failed';
507
+ target.status = WI_STATUS.FAILED;
507
508
  target.failReason = timedOut
508
509
  ? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
509
510
  : 'All fan-out agents failed';
@@ -511,11 +512,11 @@ function updateWorkItemStatus(meta, status, reason) {
511
512
  }
512
513
  } else {
513
514
  target.status = status;
514
- if (status === 'done') {
515
+ if (status === WI_STATUS.DONE) {
515
516
  delete target.failReason;
516
517
  delete target.failedAt;
517
518
  target.completedAt = ts();
518
- } else if (status === 'failed') {
519
+ } else if (status === WI_STATUS.FAILED) {
519
520
  if (reason) target.failReason = reason;
520
521
  target.failedAt = ts();
521
522
  }
@@ -643,6 +644,9 @@ function syncPrsFromOutput(output, agentId, meta, config) {
643
644
  for (const [name, { prPath, prIds }] of targetPrIds) {
644
645
  mutateJsonFileLocked(prPath, (prs) => {
645
646
  if (!Array.isArray(prs)) prs = [];
647
+ // Deduplicate any existing entries with same id (case-insensitive agent name race)
648
+ const seen = new Set();
649
+ prs = prs.filter(p => { const k = String(p.id); if (seen.has(k)) return false; seen.add(k); return true; });
646
650
  for (const { prId, fullId } of prIds) {
647
651
  if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
648
652
 
@@ -981,7 +985,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
981
985
  m.lastTask = dispatchItem.task;
982
986
  m.lastCompleted = ts();
983
987
  if (model) m.model = model;
984
- if (result === 'success') {
988
+ if (result === DISPATCH_RESULT.SUCCESS) {
985
989
  m.tasksCompleted++;
986
990
  if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
987
991
  if (dispatchItem.type === 'review') m.reviewsDone++;
@@ -1105,7 +1109,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1105
1109
  const type = dispatchItem.type;
1106
1110
  const meta = dispatchItem.meta;
1107
1111
  const isSuccess = code === 0;
1108
- const result = isSuccess ? 'success' : 'error';
1112
+ const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
1109
1113
  const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
1110
1114
 
1111
1115
  // Save session for potential resume on next dispatch
@@ -1141,9 +1145,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1141
1145
  }
1142
1146
  } catch { /* optional */ }
1143
1147
 
1144
- if (retries < 3) {
1145
- log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
1146
- updateWorkItemStatus(meta, 'pending', '');
1148
+ const maxRetries = ENGINE_DEFAULTS.maxRetries;
1149
+ if (retries < maxRetries) {
1150
+ log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
1151
+ updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
1147
1152
  try {
1148
1153
  const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1149
1154
  ? path.join(MINIONS_DIR, 'work-items.json')
@@ -1152,14 +1157,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1152
1157
  const items = safeJson(wiPath) || [];
1153
1158
  const wi = items.find(i => i.id === meta.item.id);
1154
1159
  if (wi) {
1155
- wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1156
- if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1160
+ wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
1161
+ if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
1157
1162
  shared.safeWrite(wiPath, items);
1158
1163
  }
1159
1164
  }
1160
1165
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1161
1166
  } else {
1162
- updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
1167
+ updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${maxRetries} retries exhausted)`);
1163
1168
  }
1164
1169
  // Clear _decomposing flag on failure so item doesn't get permanently stuck
1165
1170
  if (type === 'decompose') {
package/engine/shared.js CHANGED
@@ -386,8 +386,29 @@ const ENGINE_DEFAULTS = {
386
386
  evalLoop: true, // enable review→fix loop after implementation completes
387
387
  evalMaxIterations: 3, // max review→fix cycles before escalating to human
388
388
  evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
389
+ maxRetries: 3, // max dispatch retries before marking work item as failed
389
390
  };
390
391
 
392
+ // ─── Status & Type Constants ─────────────────────────────────────────────────
393
+
394
+ const WI_STATUS = {
395
+ PENDING: 'pending', DISPATCHED: 'dispatched', DONE: 'done', FAILED: 'failed',
396
+ PAUSED: 'paused', QUEUED: 'queued', NEEDS_REVIEW: 'needs-human-review', DECOMPOSED: 'decomposed',
397
+ };
398
+ const DONE_STATUSES = new Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete']); // includes legacy aliases
399
+ const WORK_TYPE = {
400
+ IMPLEMENT: 'implement', IMPLEMENT_LARGE: 'implement:large', FIX: 'fix', REVIEW: 'review',
401
+ VERIFY: 'verify', PLAN: 'plan', PLAN_TO_PRD: 'plan-to-prd', DECOMPOSE: 'decompose',
402
+ MEETING: 'meeting', EXPLORE: 'explore', ASK: 'ask', TEST: 'test', DOCS: 'docs',
403
+ };
404
+ const PLAN_STATUS = {
405
+ ACTIVE: 'active', AWAITING_APPROVAL: 'awaiting-approval', APPROVED: 'approved',
406
+ PAUSED: 'paused', REJECTED: 'rejected', COMPLETED: 'completed',
407
+ REVISION_REQUESTED: 'revision-requested',
408
+ };
409
+ const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed' };
410
+ const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout' };
411
+
391
412
  const DEFAULT_AGENTS = {
392
413
  ripley: { name: 'Ripley', emoji: '\u{1F3D7}\uFE0F', role: 'Lead / Explorer', skills: ['architecture', 'codebase-exploration', 'design-review'] },
393
414
  dallas: { name: 'Dallas', emoji: '\u{1F527}', role: 'Engineer', skills: ['implementation', 'typescript', 'docker', 'testing'] },
@@ -617,6 +638,7 @@ module.exports = {
617
638
  KB_CATEGORIES,
618
639
  classifyInboxItem,
619
640
  ENGINE_DEFAULTS,
641
+ WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
620
642
  DEFAULT_AGENTS,
621
643
  DEFAULT_CLAUDE,
622
644
  getProjects,
package/engine.js CHANGED
@@ -24,7 +24,8 @@
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 } = shared;
27
+ const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
28
+ WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, DISPATCH_RESULT } = shared;
28
29
  const queries = require('./engine/queries');
29
30
 
30
31
  // ─── Paths ──────────────────────────────────────────────────────────────────
@@ -754,7 +755,7 @@ function areDependenciesMet(item, config) {
754
755
  } catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
755
756
  }
756
757
  // PRD item statuses that count as "done" for dep resolution
757
- const PRD_MET_STATUSES = new Set(['done', 'in-pr', 'implemented', 'complete']);
758
+ const PRD_MET_STATUSES = DONE_STATUSES;
758
759
 
759
760
  for (const depId of deps) {
760
761
  const depItem = allWorkItems.find(w => w.id === depId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.314",
3
+ "version": "0.1.316",
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"
@@ -1,114 +0,0 @@
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.