@yemi33/minions 0.1.1679 → 0.1.1681

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,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1681 (2026-05-02)
4
+
5
+ ### Features
6
+ - plan completion lifecycle audit fixes (#1982)
7
+
8
+ ## 0.1.1680 (2026-05-02)
9
+
10
+ ### Other
11
+ - Improve agent playbook accuracy
12
+
3
13
  ## 0.1.1679 (2026-05-02)
4
14
 
5
15
  ### Fixes
package/dashboard.js CHANGED
@@ -2309,12 +2309,14 @@ const server = http.createServer(async (req, res) => {
2309
2309
  // If archived, temporarily restore to active so checkPlanCompletion can find it
2310
2310
  const activePath = path.join(prdDir, body.file);
2311
2311
  if (fromArchive) {
2312
- const plan = safeJson(prdPath);
2313
- if (!plan) return jsonReply(res, 500, { error: 'Could not parse PRD file' });
2314
- plan.status = 'approved';
2315
- delete plan.completedAt;
2316
- delete plan.planStale;
2317
- safeWrite(activePath, plan);
2312
+ const archivedPlan = safeJson(prdPath);
2313
+ if (!archivedPlan) return jsonReply(res, 500, { error: 'Could not parse PRD file' });
2314
+ mutateJsonFileLocked(activePath, () => {
2315
+ archivedPlan.status = 'approved';
2316
+ delete archivedPlan.completedAt;
2317
+ delete archivedPlan.planStale;
2318
+ return archivedPlan;
2319
+ }, { defaultValue: archivedPlan });
2318
2320
  }
2319
2321
 
2320
2322
  const config = queries.getConfig();
@@ -2347,11 +2349,10 @@ const server = http.createServer(async (req, res) => {
2347
2349
  }
2348
2350
 
2349
2351
  // No existing verify — clear completion flag and trigger fresh creation
2350
- const planData = safeJson(activePath);
2351
- if (planData?._completionNotified) {
2352
- planData._completionNotified = false;
2353
- safeWrite(activePath, planData);
2354
- }
2352
+ mutateJsonFileLocked(activePath, (planData) => {
2353
+ if (planData?._completionNotified) planData._completionNotified = false;
2354
+ return planData;
2355
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
2355
2356
 
2356
2357
  const lifecycle = require('./engine/lifecycle');
2357
2358
  lifecycle.checkPlanCompletion({ item: { sourcePlan: body.file, id: 'manual' } }, config);
@@ -3572,15 +3573,18 @@ const server = http.createServer(async (req, res) => {
3572
3573
  const body = await readBody(req);
3573
3574
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
3574
3575
  const planPath = resolvePlanPath(body.file);
3575
- const plan = safeJsonObj(planPath);
3576
- const wasStale = !!plan.planStale;
3577
- plan.status = 'approved';
3578
- plan.approvedAt = new Date().toISOString();
3579
- plan.approvedBy = body.approvedBy || os.userInfo().username;
3580
- delete plan.pausedAt;
3581
- delete plan.planStale;
3582
- delete plan._completionNotified;
3583
- safeWrite(planPath, plan);
3576
+ let wasStale = false;
3577
+ const plan = mutateJsonFileLocked(planPath, (data) => {
3578
+ if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
3579
+ wasStale = !!data.planStale;
3580
+ data.status = 'approved';
3581
+ data.approvedAt = new Date().toISOString();
3582
+ data.approvedBy = body.approvedBy || os.userInfo().username;
3583
+ delete data.pausedAt;
3584
+ delete data.planStale;
3585
+ delete data._completionNotified;
3586
+ return data;
3587
+ }, { defaultValue: {} });
3584
3588
 
3585
3589
  // Resume paused work items across all projects
3586
3590
  let resumed = 0;
@@ -3662,10 +3666,12 @@ const server = http.createServer(async (req, res) => {
3662
3666
  const body = await readBody(req);
3663
3667
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
3664
3668
  const planPath = resolvePlanPath(body.file);
3665
- const plan = safeJsonObj(planPath);
3666
- plan.status = 'paused';
3667
- plan.pausedAt = new Date().toISOString();
3668
- safeWrite(planPath, plan);
3669
+ mutateJsonFileLocked(planPath, (plan) => {
3670
+ if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = {};
3671
+ plan.status = 'paused';
3672
+ plan.pausedAt = new Date().toISOString();
3673
+ return plan;
3674
+ }, { defaultValue: {} });
3669
3675
 
3670
3676
  // Propagate pause to materialized work items across all projects:
3671
3677
  // kill any active agent process and reset non-completed items to paused.
@@ -3799,12 +3805,14 @@ const server = http.createServer(async (req, res) => {
3799
3805
  const body = await readBody(req);
3800
3806
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
3801
3807
  const planPath = resolvePlanPath(body.file);
3802
- const plan = safeJsonObj(planPath);
3803
- plan.status = 'rejected';
3804
- plan.rejectedAt = new Date().toISOString();
3805
- plan.rejectedBy = body.rejectedBy || os.userInfo().username;
3806
- if (body.reason) plan.rejectionReason = body.reason;
3807
- safeWrite(planPath, plan);
3808
+ const plan = mutateJsonFileLocked(planPath, (data) => {
3809
+ if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
3810
+ data.status = 'rejected';
3811
+ data.rejectedAt = new Date().toISOString();
3812
+ data.rejectedBy = body.rejectedBy || os.userInfo().username;
3813
+ if (body.reason) data.rejectionReason = body.reason;
3814
+ return data;
3815
+ }, { defaultValue: {} });
3808
3816
 
3809
3817
  // Teams notification for plan rejection — non-blocking
3810
3818
  try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-rejected').catch(() => {}); } catch {}
@@ -3956,18 +3964,23 @@ const server = http.createServer(async (req, res) => {
3956
3964
 
3957
3965
  let archivedSource = null;
3958
3966
  let plan = {};
3967
+ const archiveWarnings = [];
3959
3968
  if (isPrd) {
3960
3969
  try {
3961
- plan = safeJsonObj(archivePath) || {};
3962
- plan.status = 'archived';
3963
- plan.archivedAt = new Date().toISOString();
3964
- safeWrite(archivePath, plan);
3970
+ plan = mutateJsonFileLocked(archivePath, (data) => {
3971
+ if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
3972
+ data.status = 'archived';
3973
+ data.archivedAt = new Date().toISOString();
3974
+ return data;
3975
+ }, { defaultValue: {} }) || {};
3965
3976
  // Without removing the .backup sidecar, safeJson would auto-restore the
3966
3977
  // pre-completion snapshot on engine restart, re-triggering plan completion
3967
3978
  // and spawning duplicate verify tasks (regression of #f28162b0).
3968
- const backupPath = planPath + '.backup';
3969
- try { fs.unlinkSync(backupPath); } catch {
3970
- try { fs.writeFileSync(backupPath, JSON.stringify({ status: 'archived' })); } catch { /* best-effort */ }
3979
+ const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
3980
+ if (!backupCleanup.ok) {
3981
+ const warning = `Archive backup cleanup failed for ${body.file}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`;
3982
+ archiveWarnings.push(warning);
3983
+ console.warn(warning);
3971
3984
  }
3972
3985
  if (plan.source_plan) {
3973
3986
  const mdPath = path.join(PLANS_DIR, plan.source_plan);
@@ -4006,7 +4019,9 @@ const server = http.createServer(async (req, res) => {
4006
4019
  } catch (e) { console.error('plan worktree cleanup:', e.message); }
4007
4020
 
4008
4021
  invalidateStatusCache();
4009
- return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource, cancelledItems });
4022
+ const payload = { ok: true, archived: body.file, archivedSource, cancelledItems };
4023
+ if (archiveWarnings.length > 0) payload.warnings = archiveWarnings;
4024
+ return jsonReply(res, 200, payload);
4010
4025
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
4011
4026
  }
4012
4027
 
@@ -4047,12 +4062,14 @@ const server = http.createServer(async (req, res) => {
4047
4062
  const body = await readBody(req);
4048
4063
  if (!body.file || !body.feedback) return jsonReply(res, 400, { error: 'file and feedback required' });
4049
4064
  const planPath = resolvePlanPath(body.file);
4050
- const plan = safeJsonObj(planPath);
4051
- plan.status = 'revision-requested';
4052
- plan.revision_feedback = body.feedback;
4053
- plan.revisionRequestedAt = new Date().toISOString();
4054
- plan.revisionRequestedBy = body.requestedBy || os.userInfo().username;
4055
- safeWrite(planPath, plan);
4065
+ const plan = mutateJsonFileLocked(planPath, (data) => {
4066
+ if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
4067
+ data.status = 'revision-requested';
4068
+ data.revision_feedback = body.feedback;
4069
+ data.revisionRequestedAt = new Date().toISOString();
4070
+ data.revisionRequestedBy = body.requestedBy || os.userInfo().username;
4071
+ return data;
4072
+ }, { defaultValue: {} });
4056
4073
 
4057
4074
  // Create a work item to revise the plan
4058
4075
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
@@ -6,8 +6,8 @@ How plans go from idea to verified, running code.
6
6
 
7
7
  ```
8
8
  /plan "feature description"
9
- → agent writes plan JSON (plan-*.json)
10
- engine chains plan PRD (plan-to-prd)
9
+ → agent writes plan markdown (plans/plan-*.md)
10
+ human reviews and explicitly runs plan-to-prd
11
11
  → agent converts plan into structured PRD items with acceptance criteria
12
12
  → engine materializes PRD items as work items (PL-W001, PL-W002, ...)
13
13
  → engine dispatches work items respecting dependency order
@@ -15,6 +15,7 @@ How plans go from idea to verified, running code.
15
15
  → engine auto-reviews PRs, handles feedback loops
16
16
  → all items done → engine creates VERIFY task
17
17
  → verify agent builds everything, starts webapp, writes testing guide
18
+ → human archives the PRD/plan from the dashboard when ready
18
19
  ```
19
20
 
20
21
  ## Dependency Management
@@ -66,7 +67,7 @@ When all PRD items reach `done` or `failed` status, `checkPlanCompletion()` fire
66
67
  1. **Completion summary** written to `notes/inbox/` with results, timing, and PR list
67
68
  2. **Verification task** created (type `verify`, priority `high`) — see below
68
69
  3. **PR work item** created for shared-branch plans (to merge the feature branch)
69
- 4. **Plan archived** to `plans/archive/`
70
+ 4. **PRD status** persisted as `completed` with `_completionNotified`; files stay active until manual archive
70
71
 
71
72
  ## Verification Task
72
73
 
@@ -109,6 +110,21 @@ git merge "origin/feat/PL-W005" --no-edit
109
110
 
110
111
  The verify agent and the user can then build and run from these paths.
111
112
 
113
+ ## Manual Archive Lifecycle
114
+
115
+ Archiving is a deliberate dashboard action, not a post-verify side effect. When a PRD completes, `checkPlanCompletion()` writes the completion summary, persists `status: "completed"` plus `_completionNotified`, and creates the verify work item. The PRD remains in `prd/` and its source plan remains in `plans/` so humans can review the guide, inspect the final state, resume stale work, or reopen items before hiding the plan from active views.
116
+
117
+ The verify agent does not call `archivePlan()`. After the testing guide and any follow-up PR work are satisfactory, use the dashboard archive action, which calls `POST /api/plans/archive` with the active PRD or plan filename.
118
+
119
+ Manual archive behavior:
120
+
121
+ 1. For PRD JSON files, the dashboard moves `prd/<file>.json` to `prd/archive/<file>.json`, marks the archived JSON with `status: "archived"` and `archivedAt`, and removes or neutralizes the active `.backup` sidecar so `safeJson()` cannot restore a stale completed PRD on restart.
122
+ 2. If the PRD has `source_plan`, the matching `plans/<source>.md` is moved to `plans/archive/<source>.md`.
123
+ 3. Pending or queued work items linked to the archived PRD are cancelled with `_cancelledBy: "plan-archived"`; completed work items remain as history.
124
+ 4. Plan worktrees are cleaned up by the archive handler/lifecycle cleanup path.
125
+
126
+ Use unarchive only when the archived PRD/plan should reappear in active views. Reopening completed work should happen before archive, or after unarchive, so the materializer can see the active PRD file.
127
+
112
128
  ## Human Feedback on PRs
113
129
 
114
130
  After PRs are created, humans can leave comments containing `@minions` to trigger fix tasks. If you're the only human commenting, any comment triggers a fix — no keyword needed. See `pollPrHumanComments()` in `engine/ado.js`.
@@ -130,11 +146,11 @@ After PRs are created, humans can leave comments containing `@minions` to trigge
130
146
 
131
147
  | File | Purpose |
132
148
  |------|---------|
133
- | `plans/*.json` | Active plan files with PRD items |
134
- | `plans/archive/` | Completed/archived plans |
149
+ | `plans/*.md` | Source plans awaiting review or linked to active PRDs |
150
+ | `prd/*.json` | Active PRDs with materializable items |
151
+ | `plans/archive/`, `prd/archive/` | Manually archived plans and PRDs |
135
152
  | `playbooks/verify.md` | Verification task playbook |
136
153
  | `playbooks/implement.md` | Implementation playbook |
137
154
  | `playbooks/plan-to-prd.md` | Plan → PRD conversion playbook |
138
- | `engine/lifecycle.js` | `checkPlanCompletion`, `chainPlanToPrd` |
155
+ | `engine/lifecycle.js` | `checkPlanCompletion`, completion hooks, PRD sync |
139
156
  | `engine.js` | `spawnAgent` (dependency merging), `resolveDependencyBranches` |
140
-
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T05:46:43.671Z"
4
+ "cachedAt": "2026-05-02T13:35:00.752Z"
5
5
  }
@@ -69,6 +69,12 @@ function checkPlanCompletion(meta, config) {
69
69
 
70
70
  const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
71
71
  const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED || w.status === WI_STATUS.CANCELLED);
72
+ const isActiveVerify = (wi) => wi && (
73
+ wi.status === WI_STATUS.PENDING ||
74
+ wi.status === WI_STATUS.QUEUED ||
75
+ wi.status === WI_STATUS.DISPATCHED
76
+ );
77
+ const isReopenableVerify = (wi) => wi && (DONE_STATUSES.has(wi.status) || wi.status === WI_STATUS.FAILED);
72
78
 
73
79
  if (failedItems.length > 0) {
74
80
  const failDetails = failedItems.map(w =>
@@ -166,6 +172,7 @@ function checkPlanCompletion(meta, config) {
166
172
  const mainBranch = shared.resolveMainBranch(primaryProject.localPath, primaryProject.mainBranch);
167
173
  const itemSummary = doneItems.map(w => '- ' + w.id + ': ' + w.title.replace('Implement: ', '')).join('\n');
168
174
  mutateWorkItems(wiPath, workItems => {
175
+ if (workItems.some(w => w.sourcePlan === planFile && w.itemType === 'pr')) return workItems;
169
176
  workItems.push({
170
177
  id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
171
178
  type: 'implement', priority: 'high',
@@ -181,9 +188,23 @@ function checkPlanCompletion(meta, config) {
181
188
  // 4. Create verification work item (build, test, start webapp, write testing guide)
182
189
  // Only one verify per PRD — skip if pending/dispatched, re-open if done/failed (PRD was modified)
183
190
  const existingVerify = allWorkItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
184
- if (existingVerify && (existingVerify.status === WI_STATUS.PENDING || existingVerify.status === WI_STATUS.DISPATCHED)) {
191
+ if (isActiveVerify(existingVerify)) {
185
192
  log('info', `Plan ${planFile}: verify WI ${existingVerify.id} already ${existingVerify.status} — skipping`);
186
- } else if (doneItems.length > 0) {
193
+ } else if (isReopenableVerify(existingVerify) && doneItems.length > 0) {
194
+ const verifyProject = existingVerify.project || projectName;
195
+ const vWiPath = shared.projectWorkItemsPath(
196
+ projects.find(p => p.name?.toLowerCase() === verifyProject?.toLowerCase()) || primaryProject
197
+ );
198
+ let reopenedVerify = false;
199
+ mutateWorkItems(vWiPath, items => {
200
+ const v = items.find(w => w.id === existingVerify.id);
201
+ if (isReopenableVerify(v)) {
202
+ shared.reopenWorkItem(v);
203
+ reopenedVerify = true;
204
+ }
205
+ });
206
+ if (reopenedVerify) log('info', `Re-opened verification work item ${existingVerify.id} for modified plan ${planFile}`);
207
+ } else if (!existingVerify && doneItems.length > 0) {
187
208
  const verifyId = 'PL-' + shared.uid();
188
209
  const planSlug = planFile.replace('.json', '');
189
210
 
@@ -276,7 +297,17 @@ function checkPlanCompletion(meta, config) {
276
297
  prSummary,
277
298
  ].join('\n');
278
299
 
300
+ let createdVerify = false;
301
+ let reopenedVerifyId = null;
279
302
  mutateWorkItems(wiPath, workItems => {
303
+ const v = workItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
304
+ if (v) {
305
+ if (isReopenableVerify(v)) {
306
+ shared.reopenWorkItem(v);
307
+ reopenedVerifyId = v.id;
308
+ }
309
+ return workItems;
310
+ }
280
311
  workItems.push({
281
312
  id: verifyId,
282
313
  title: `Verify plan: ${(plan.plan_summary || planFile).slice(0, 80)}`,
@@ -290,32 +321,19 @@ function checkPlanCompletion(meta, config) {
290
321
  itemType: 'verify',
291
322
  project: projectName,
292
323
  });
324
+ createdVerify = true;
293
325
  });
294
- log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
326
+ if (createdVerify) {
327
+ log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
295
328
 
296
- // Teams notification for verify creation — non-blocking
297
- try {
298
- const teams = require('./teams');
299
- teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
300
- } catch {}
301
- } else if (existingVerify && DONE_STATUSES.has(existingVerify.status) && doneItems.length > 0) {
302
- // PRD was modified and re-completed re-open the existing verify instead of creating a duplicate
303
- const verifyProject = existingVerify.project || projectName;
304
- const vWiPath = shared.projectWorkItemsPath(
305
- projects.find(p => p.name?.toLowerCase() === verifyProject?.toLowerCase()) || primaryProject
306
- );
307
- mutateWorkItems(vWiPath, items => {
308
- const v = items.find(w => w.id === existingVerify.id);
309
- if (v && DONE_STATUSES.has(v.status)) {
310
- v.status = WI_STATUS.PENDING;
311
- v._reopened = true;
312
- delete v.completedAt;
313
- delete v.dispatched_to;
314
- delete v.dispatched_at;
315
- v._retryCount = 0;
316
- }
317
- });
318
- log('info', `Re-opened verification work item ${existingVerify.id} for modified plan ${planFile}`);
329
+ // Teams notification for verify creation — non-blocking
330
+ try {
331
+ const teams = require('./teams');
332
+ teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
333
+ } catch {}
334
+ } else if (reopenedVerifyId) {
335
+ log('info', `Re-opened verification work item ${reopenedVerifyId} for modified plan ${planFile}`);
336
+ }
319
337
  }
320
338
 
321
339
  // Archive deferred until verify completes
@@ -350,9 +368,9 @@ function archivePlan(planFile, plan, projects, config) {
350
368
  // plan completion and spawning duplicate verify tasks for already-archived plans.
351
369
  // On Windows, the unlink can fail due to file locking; overwrite with archived status
352
370
  // as a fallback so a restored backup is inert even if deletion fails.
353
- const backupPath = planPath + '.backup';
354
- try { fs.unlinkSync(backupPath); } catch {
355
- try { fs.writeFileSync(backupPath, JSON.stringify({ status: 'archived' })); } catch { }
371
+ const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
372
+ if (!backupCleanup.ok) {
373
+ log('warn', `Archive backup cleanup failed for ${planFile}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`);
356
374
  }
357
375
  } catch (err) {
358
376
  log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
@@ -440,95 +458,6 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
440
458
  } catch (err) { log('warn', `Plan worktree cleanup: ${err.message}`); }
441
459
  }
442
460
 
443
- // ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
444
- function chainPlanToPrd(dispatchItem, meta, config) {
445
-
446
- const planDir = path.join(MINIONS_DIR, 'plans');
447
- if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
448
-
449
- let planFileName = meta?.planFileName || meta?.item?._planFileName;
450
- if (planFileName && fs.existsSync(path.join(planDir, planFileName))) {
451
- // Exact match from meta
452
- } else {
453
- const planFiles = fs.readdirSync(planDir)
454
- .filter(f => f.endsWith('.md') || f.endsWith('.json'))
455
- .map(f => ({ name: f, mtime: fs.statSync(path.join(planDir, f)).mtimeMs }))
456
- .sort((a, b) => b.mtime - a.mtime);
457
- planFileName = planFiles[0]?.name;
458
- if (!planFileName) {
459
- log('warn', `Plan chaining: no plan files found in plans/ after task ${dispatchItem.id}`);
460
- return;
461
- }
462
- log('info', `Plan chaining: using mtime fallback — found ${planFileName}`);
463
- }
464
-
465
- if (planFileName.endsWith('.json')) {
466
- const mdName = planFileName.replace(/\.json$/, '.md');
467
- // Check plans/ first, then prd/ for .json files
468
- const jsonPath = fs.existsSync(path.join(planDir, planFileName))
469
- ? path.join(planDir, planFileName)
470
- : path.join(MINIONS_DIR, 'prd', planFileName);
471
- const mdPath = path.join(planDir, mdName);
472
- try {
473
- const content = fs.readFileSync(jsonPath, 'utf8');
474
- const parsed = JSON.parse(content);
475
- if (!parsed.missing_features) {
476
- fs.renameSync(jsonPath, mdPath);
477
- planFileName = mdName;
478
- log('info', `Plan chaining: renamed ${planFileName} → ${mdName} (plans must be .md)`);
479
- }
480
- } catch {
481
- try {
482
- if (fs.existsSync(jsonPath)) fs.renameSync(jsonPath, path.join(planDir, mdName));
483
- planFileName = mdName;
484
- log('info', `Plan chaining: renamed to .md (not valid JSON)`);
485
- } catch (err) { log('warn', `Plan rename fallback: ${err.message}`); }
486
- }
487
- }
488
-
489
- const planFile = { name: planFileName };
490
- const planPath = path.join(planDir, planFileName);
491
- let planContent;
492
- try { planContent = fs.readFileSync(planPath, 'utf8'); } catch (err) {
493
- log('error', `Plan chaining: failed to read plan file ${planFile.name}: ${err.message}`);
494
- return;
495
- }
496
-
497
- const projectName = meta?.item?.project || meta?.project?.name;
498
- const projects = shared.getProjects(config);
499
- if (projects.length === 0) {
500
- log('error', 'Plan chaining: no projects configured');
501
- return;
502
- }
503
- const targetProject = projectName
504
- ? projects.find(p => p.name === projectName) || projects[0]
505
- : projects[0];
506
-
507
- if (!targetProject) {
508
- log('error', 'Plan chaining: no target project available');
509
- return;
510
- }
511
-
512
- log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
513
- const wiPath = path.join(MINIONS_DIR, 'work-items.json');
514
- shared.mutateJsonFileLocked(wiPath, (items) => {
515
- if (!Array.isArray(items)) items = [];
516
- items.push({
517
- id: 'W-' + shared.uid(),
518
- title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
519
- type: 'plan-to-prd',
520
- priority: meta?.item?.priority || 'high',
521
- description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
522
- status: WI_STATUS.PENDING,
523
- created: ts(),
524
- createdBy: 'engine:chain',
525
- project: targetProject.name,
526
- planFile: planFile.name,
527
- });
528
- return items;
529
- }, { defaultValue: [] });
530
- }
531
-
532
461
  // ─── Work Item Path Resolution ───────────────────────────────────────────────
533
462
 
534
463
  /** Resolve the work-items.json path from dispatch meta. Reused by retry paths. */
@@ -642,15 +571,14 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
642
571
  const feature = plan?.missing_features?.find(f => f.id === itemId);
643
572
  if (!feature || feature.status === status) continue;
644
573
  let updated = false;
645
- shared.withFileLock(`${fpath}.lock`, () => {
646
- const fresh = safeJson(fpath);
574
+ mutateJsonFileLocked(fpath, (fresh) => {
647
575
  const f = fresh?.missing_features?.find(x => x.id === itemId);
648
576
  if (f && f.status !== status) {
649
577
  f.status = status;
650
- safeWrite(fpath, fresh);
651
578
  updated = true;
652
579
  }
653
- });
580
+ return fresh;
581
+ }, { skipWriteIfUnchanged: true });
654
582
  if (updated) return;
655
583
  }
656
584
  } catch (err) { log('warn', `PRD status sync: ${err.message}`); }
@@ -682,31 +610,28 @@ function reconcilePrdStatuses(config) {
682
610
  for (const file of prdFiles) {
683
611
  try {
684
612
  const fpath = path.join(PRD_DIR, file);
685
- const plan = safeJson(fpath);
686
- if (!plan?.missing_features) continue;
687
- // Skip completed/archived PRDs — no reconciliation needed
688
- if (plan.status === PLAN_STATUS.COMPLETED) continue;
689
-
690
- let modified = false;
691
- for (const feature of plan.missing_features) {
692
- if (feature.status === PRD_ITEM_STATUS.MISSING && doneWiById.has(feature.id)) {
693
- feature.status = PRD_ITEM_STATUS.UPDATED;
694
- modified = true;
695
- log('info', `PRD backward-scan: promoted ${feature.id} from missing→updated in ${file} (done work item exists)`);
696
- }
697
- // (#984) Stale status: PRD item stuck at dispatched/failed/pending while WI is done —
698
- // happens when fix work items complete with a different ID than the original PRD feature
699
- else if (_STALE_PRD_STATUSES.has(feature.status) && doneWiById.has(feature.id)) {
700
- const prev = feature.status;
701
- feature.status = WI_STATUS.DONE;
702
- modified = true;
703
- log('info', `PRD backward-scan: promoted ${feature.id} from ${prev}→done in ${file} (done work item exists)`);
704
- }
705
- }
613
+ const logMessages = [];
614
+ mutateJsonFileLocked(fpath, (plan) => {
615
+ if (!plan?.missing_features) return plan;
616
+ // Skip completed/archived PRDs — no reconciliation needed
617
+ if (plan.status === PLAN_STATUS.COMPLETED) return plan;
706
618
 
707
- if (modified) {
708
- safeWrite(fpath, plan);
709
- }
619
+ for (const feature of plan.missing_features) {
620
+ if (feature.status === PRD_ITEM_STATUS.MISSING && doneWiById.has(feature.id)) {
621
+ feature.status = PRD_ITEM_STATUS.UPDATED;
622
+ logMessages.push(`PRD backward-scan: promoted ${feature.id} from missing→updated in ${file} (done work item exists)`);
623
+ }
624
+ // (#984) Stale status: PRD item stuck at dispatched/failed/pending while WI is done —
625
+ // happens when fix work items complete with a different ID than the original PRD feature
626
+ else if (_STALE_PRD_STATUSES.has(feature.status) && doneWiById.has(feature.id)) {
627
+ const prev = feature.status;
628
+ feature.status = WI_STATUS.DONE;
629
+ logMessages.push(`PRD backward-scan: promoted ${feature.id} from ${prev}→done in ${file} (done work item exists)`);
630
+ }
631
+ }
632
+ return plan;
633
+ }, { skipWriteIfUnchanged: true });
634
+ for (const message of logMessages) log('info', message);
710
635
  } catch (err) { log('warn', `PRD backward-scan for ${file}: ${err.message}`); }
711
636
  }
712
637
  }
@@ -1608,19 +1533,17 @@ async function handlePostMerge(pr, project, config, newStatus) {
1608
1533
  const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
1609
1534
  let updated = 0;
1610
1535
  for (const pf of planFiles) {
1611
- const plan = safeJson(path.join(prdDir, pf));
1612
- if (!plan?.missing_features) continue;
1613
- let changed = false;
1614
- for (const feature of plan.missing_features) {
1615
- if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
1616
- feature.status = WI_STATUS.DONE;
1617
- changed = true;
1618
- updated++;
1536
+ const planPath = path.join(prdDir, pf);
1537
+ mutateJsonFileLocked(planPath, (plan) => {
1538
+ if (!plan?.missing_features) return plan;
1539
+ for (const feature of plan.missing_features) {
1540
+ if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
1541
+ feature.status = WI_STATUS.DONE;
1542
+ updated++;
1543
+ }
1619
1544
  }
1620
- }
1621
- if (changed) {
1622
- shared.safeWrite(path.join(prdDir, pf), plan);
1623
- }
1545
+ return plan;
1546
+ }, { skipWriteIfUnchanged: true });
1624
1547
  }
1625
1548
  if (updated > 0) log('info', `Post-merge: marked ${mergedItemIds.join(', ')} as done for ${pr.id}`);
1626
1549
  } catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
package/engine/shared.js CHANGED
@@ -212,6 +212,33 @@ function safeUnlink(p) {
212
212
  try { fs.unlinkSync(p); } catch { /* cleanup */ }
213
213
  }
214
214
 
215
+ function neutralizeJsonBackupSidecar(filePath, inertData = { status: 'archived' }) {
216
+ const backupPath = filePath + '.backup';
217
+ try {
218
+ fs.unlinkSync(backupPath);
219
+ return { ok: true, action: 'removed', backupPath };
220
+ } catch (unlinkErr) {
221
+ if (unlinkErr.code === 'ENOENT') return { ok: true, action: 'absent', backupPath };
222
+ try {
223
+ safeWrite(backupPath, inertData);
224
+ return {
225
+ ok: true,
226
+ action: 'neutralized',
227
+ backupPath,
228
+ unlinkError: unlinkErr.message,
229
+ };
230
+ } catch (writeErr) {
231
+ return {
232
+ ok: false,
233
+ action: 'failed',
234
+ backupPath,
235
+ unlinkError: unlinkErr.message,
236
+ writeError: writeErr.message,
237
+ };
238
+ }
239
+ }
240
+ }
241
+
215
242
  // ── Dispatch Prompt Sidecar (#1167) ─────────────────────────────────────────
216
243
  // Large prompts (PR diffs, build error logs, coalesced human feedback) inlined
217
244
  // into dispatch.json caused hundreds-of-MB bloat per entry and eventual V8 OOM
@@ -2387,6 +2414,7 @@ module.exports = {
2387
2414
  safeJson, safeJsonObj, safeJsonArr,
2388
2415
  safeWrite,
2389
2416
  safeUnlink,
2417
+ neutralizeJsonBackupSidecar,
2390
2418
  PROMPT_CONTEXTS_DIR,
2391
2419
  dispatchPromptSidecarPath,
2392
2420
  dispatchCompletionReportPath,
package/engine.js CHANGED
@@ -1767,6 +1767,16 @@ function buildWiDescription(item, planFile) {
1767
1767
 
1768
1768
  function materializePlansAsWorkItems(config) {
1769
1769
  if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
1770
+ const writePrdLocked = (fileName, data) => {
1771
+ return mutateJsonFileLocked(path.join(PRD_DIR, fileName), () => data, { defaultValue: data });
1772
+ };
1773
+ const mutatePrdLocked = (fileName, fallback, mutator, options = {}) => {
1774
+ return mutateJsonFileLocked(path.join(PRD_DIR, fileName), (current) => {
1775
+ if (!current?.missing_features && fallback?.missing_features) current = fallback;
1776
+ if (!current || Array.isArray(current) || typeof current !== 'object') current = {};
1777
+ return mutator(current) || current;
1778
+ }, { defaultValue: fallback || {}, ...options });
1779
+ };
1770
1780
 
1771
1781
  // Enforce: PRDs must be .json — auto-rename .md files that contain valid PRD JSON
1772
1782
  // Check both prd/ and plans/ (agents may still write JSON to plans/)
@@ -1782,7 +1792,7 @@ function materializePlansAsWorkItems(config) {
1782
1792
  const parsed = JSON.parse(stripped);
1783
1793
  if (parsed.missing_features) {
1784
1794
  const jsonName = mf.replace(/\.md$/, '.json');
1785
- safeWrite(path.join(PRD_DIR, jsonName), parsed);
1795
+ writePrdLocked(jsonName, parsed);
1786
1796
  try { fs.unlinkSync(path.join(checkDir, mf)); } catch { /* cleanup */ }
1787
1797
  log('info', `Plan enforcement: moved ${mf} → prd/${jsonName} (PRDs must be .json in prd/)`);
1788
1798
  }
@@ -1797,7 +1807,7 @@ function materializePlansAsWorkItems(config) {
1797
1807
  try {
1798
1808
  const parsed = safeJson(path.join(PLANS_DIR, jf));
1799
1809
  if (parsed?.missing_features) {
1800
- safeWrite(path.join(PRD_DIR, jf), parsed);
1810
+ writePrdLocked(jf, parsed);
1801
1811
  try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch { /* cleanup */ }
1802
1812
  log('info', `Auto-migrated PRD ${jf} from plans/ to prd/`);
1803
1813
  }
@@ -1814,7 +1824,7 @@ function materializePlansAsWorkItems(config) {
1814
1824
  const SEQUENTIAL_ID_RE = /^P-?\d+$/;
1815
1825
 
1816
1826
  for (const file of planFiles) {
1817
- const plan = safeJson(path.join(PRD_DIR, file));
1827
+ let plan = safeJson(path.join(PRD_DIR, file));
1818
1828
  if (!plan?.missing_features) continue;
1819
1829
 
1820
1830
  // ID collision prevention: remap sequential IDs (P-001, P-002) to globally unique P-<uid> IDs.
@@ -1826,19 +1836,25 @@ function materializePlansAsWorkItems(config) {
1826
1836
  const anyMaterialized = plan.missing_features.some(f =>
1827
1837
  SEQUENTIAL_ID_RE.test(f.id) && allWorkItems.some(w => w.id === f.id && w.sourcePlan === file));
1828
1838
  if (!anyMaterialized) {
1829
- const idMap = new Map();
1830
- for (const f of plan.missing_features) {
1831
- if (SEQUENTIAL_ID_RE.test(f.id)) {
1832
- const newId = 'P-' + shared.uid();
1833
- idMap.set(f.id, newId);
1834
- f.id = newId;
1839
+ let remappedCount = 0;
1840
+ plan = mutatePrdLocked(file, plan, (current) => {
1841
+ const features = Array.isArray(current.missing_features) ? current.missing_features : [];
1842
+ if (!features.some(f => SEQUENTIAL_ID_RE.test(f.id))) return current;
1843
+ const idMap = new Map();
1844
+ for (const f of features) {
1845
+ if (SEQUENTIAL_ID_RE.test(f.id)) {
1846
+ const newId = 'P-' + shared.uid();
1847
+ idMap.set(f.id, newId);
1848
+ f.id = newId;
1849
+ }
1835
1850
  }
1836
- }
1837
- for (const f of plan.missing_features) {
1838
- if (f.depends_on) f.depends_on = f.depends_on.map(d => idMap.get(d) || d);
1839
- }
1840
- safeWrite(path.join(PRD_DIR, file), plan);
1841
- log('info', `Remapped ${idMap.size} sequential ID(s) in ${file} to prevent cross-PRD collisions`);
1851
+ for (const f of features) {
1852
+ if (f.depends_on) f.depends_on = f.depends_on.map(d => idMap.get(d) || d);
1853
+ }
1854
+ remappedCount = idMap.size;
1855
+ return current;
1856
+ });
1857
+ if (remappedCount > 0) log('info', `Remapped ${remappedCount} sequential ID(s) in ${file} to prevent cross-PRD collisions`);
1842
1858
  }
1843
1859
  }
1844
1860
  } catch (e) { log('warn', `Sequential ID remapping failed for ${file}: ${e.message}`); }
@@ -1851,25 +1867,26 @@ function materializePlansAsWorkItems(config) {
1851
1867
  const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
1852
1868
  if (!recorded) {
1853
1869
  // First time seeing this plan — record baseline mtime (no clean needed)
1854
- plan.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
1855
- safeWrite(path.join(PRD_DIR, file), plan);
1870
+ plan = mutatePrdLocked(file, plan, (current) => {
1871
+ if (!current.sourcePlanModifiedAt) current.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
1872
+ return current;
1873
+ }, { skipWriteIfUnchanged: true });
1856
1874
  } else if (sourceMtime > recorded) {
1857
1875
  // Source plan changed — auto-clean pending/failed items so they re-materialize with updated data
1858
1876
  log('info', `Source plan ${plan.source_plan} updated — re-syncing PRD ${file}`);
1859
1877
  autoCleanPrdWorkItems(file, config);
1860
- plan.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
1861
- plan.lastSyncedFromPlan = ts();
1862
1878
 
1863
1879
  // Handle PRD based on current status
1864
1880
  const prdStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
1865
1881
 
1866
- // Flag stale for all statuses — user decides when to regenerate/resume from dashboard
1867
- if (prdStatus) {
1868
- plan.planStale = true;
1869
- log('info', `PRD ${file} flagged as stale (plan revised while ${prdStatus}) — user can regenerate from dashboard`);
1870
- }
1871
-
1872
- safeWrite(path.join(PRD_DIR, file), plan);
1882
+ plan = mutatePrdLocked(file, plan, (current) => {
1883
+ current.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
1884
+ current.lastSyncedFromPlan = ts();
1885
+ const currentPrdStatus = current.status || (current.requires_approval ? 'awaiting-approval' : null);
1886
+ if (currentPrdStatus) current.planStale = true;
1887
+ return current;
1888
+ });
1889
+ if (prdStatus) log('info', `PRD ${file} flagged as stale (plan revised while ${prdStatus}) — user can regenerate from dashboard`);
1873
1890
  }
1874
1891
  } catch (e) { log('warn', 'plan staleness check: ' + e.message); }
1875
1892
  }
@@ -1879,10 +1896,12 @@ function materializePlansAsWorkItems(config) {
1879
1896
  const planStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
1880
1897
  if (planStatus === 'awaiting-approval') {
1881
1898
  if (config.engine?.autoApprovePlans) {
1882
- plan.status = 'approved';
1883
- plan.approvedAt = ts();
1884
- plan.approvedBy = 'auto-mode';
1885
- safeWrite(path.join(PRD_DIR, file), plan);
1899
+ plan = mutatePrdLocked(file, plan, (current) => {
1900
+ current.status = 'approved';
1901
+ current.approvedAt = ts();
1902
+ current.approvedBy = 'auto-mode';
1903
+ return current;
1904
+ });
1886
1905
  log('info', `Auto-approved plan: ${file}`);
1887
1906
  } else {
1888
1907
  continue; // Skip — waiting for human approval
@@ -3389,7 +3408,7 @@ function discoverCentralWorkItems(config) {
3389
3408
  vars.plan_file = planFileName;
3390
3409
  vars.task_description = item.title;
3391
3410
  // Notes already populated by buildWorkItemDispatchVars — no need to re-read
3392
- // Track expected plan filename in meta for chainPlanToPrd
3411
+ // Track expected plan filename for artifacts and follow-up plan-to-prd prompts.
3393
3412
  mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _planFileName: planFileName }));
3394
3413
  }
3395
3414
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1679",
3
+ "version": "0.1.1681",
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"
package/playbooks/fix.md CHANGED
@@ -17,6 +17,14 @@ Branch: `{{pr_branch}}`
17
17
 
18
18
  {{review_note}}
19
19
 
20
+ ## Finding Triage
21
+
22
+ Before editing, split the feedback into:
23
+
24
+ - **Blocking findings to fix:** correctness, safety, build/test failure, missing requested behavior, broken compatibility, or review comments explicitly required for approval.
25
+ - **Findings to answer with rationale:** comments where the current approach is intentionally correct, the reviewer misunderstood the code, or the requested change would broaden the PR beyond its purpose.
26
+ - **Non-blocking suggestions:** style, optional refactors, extra docs, or enhancements that are not required for approval. Do not implement these unless they are necessary to resolve a blocking issue.
27
+
20
28
  ## Health Check
21
29
 
22
30
  Before starting work, run `git status` and verify the worktree is clean and on the expected branch (`{{pr_branch}}`). If the worktree is dirty or on the wrong branch, report the issue and stop.
@@ -42,6 +50,7 @@ Before pushing, prove the review fix did not break the branch:
42
50
 
43
51
  - Use the project's source of truth for commands: `CLAUDE.md`, README, package scripts, Makefile, or equivalent build config.
44
52
  - Run checks that are relevant to the addressed findings. Prefer the full suite when practical.
53
+ - Capture the exact commands run and meaningful results in the PR comment and completion report.
45
54
  - Fix regressions you introduced. If failures are pre-existing or unrelated, capture the evidence and include it in the PR comment.
46
55
  - Do not push code that breaks existing tests or the build because of your changes.
47
56
 
@@ -34,11 +34,21 @@ Before starting work, run `git status` and verify the worktree is clean and on t
34
34
 
35
35
  Use subagents only for genuinely parallel, independent tasks (e.g., editing files in unrelated modules simultaneously). For sequential work, single-file edits, searches, and file reads, work directly — do not spawn subagents.
36
36
 
37
+ ## Context Discovery
38
+
39
+ Before editing, assemble a small, dependency-aware context pack:
40
+
41
+ - Read project instructions first (`CLAUDE.md`, README, package scripts, Makefile, or equivalent).
42
+ - Identify candidate files from the task text, existing symbols, comparable implementations, direct imports/callers, and corresponding tests.
43
+ - Read the smallest useful set of files first (usually 5-8). Expand only when a concrete question, failing validation, or missing pattern requires it.
44
+ - For large files, read imports, exported/public entry points, and task-relevant sections before reading the whole file.
45
+
37
46
  ## Delivery Contract
38
47
 
39
48
  Deliver this as if the user asked you directly in a CLI:
40
49
 
41
50
  - Understand the requested behavior and relevant acceptance criteria before editing.
51
+ - State the likely files to touch, patterns to follow, and main risks to yourself before making the first code change.
42
52
  - Read the smallest useful set of source, tests, docs, and comparable implementations needed to make the change correctly.
43
53
  - Follow existing project conventions, including logging, typing, error handling, and test structure.
44
54
  - Make the complete change required by the task; do not add unrelated cleanups or speculative improvements.
@@ -55,6 +65,7 @@ Before publishing, prove the change with the repo's own documented checks:
55
65
 
56
66
  - Use the project's source of truth for commands: `CLAUDE.md`, README, package scripts, Makefile, or equivalent build config.
57
67
  - Run the checks that are relevant to this task, including tests that cover the changed behavior. Prefer the full suite when practical.
68
+ - Capture the exact commands run and the meaningful result in the PR description or completion report. Do not summarize validation as "tests passed" without naming what ran.
58
69
  - Fix regressions you introduced. If failures are pre-existing or outside the task, capture the evidence and make that explicit in the PR.
59
70
  - Do not publish changes with a broken build or failing tests that you introduced.
60
71
 
package/playbooks/plan.md CHANGED
@@ -28,12 +28,14 @@ A user has described a feature they want built. Your job is to create a detailed
28
28
  - Map the areas of code that this feature will touch
29
29
  - Identify existing patterns, conventions, and extension points
30
30
  - Note dependencies and potential conflicts with in-progress work
31
+ - Build a concise context pack before planning: start with 5-15 candidate files from paths, symbols, comparable implementations, imports/callers, and tests; read the smallest useful set (usually 5-8 files) and expand only for specific unknowns
31
32
 
32
33
  ### 3. Design the Approach
33
34
  - Outline the high-level architecture for the feature
34
35
  - Identify what needs to be created vs modified
35
36
  - Consider edge cases, error handling, and backwards compatibility
36
37
  - Note any prerequisites or migrations needed
38
+ - Record non-obvious design decisions with the alternatives considered and why the chosen option best fits existing architecture
37
39
 
38
40
  ### 4. Break Down into Work Items
39
41
  - Decompose into discrete, PR-sized chunks of work
@@ -23,7 +23,14 @@ Use subagents only for genuinely parallel, independent tasks (e.g., reviewing un
23
23
  git diff {{main_branch}}...origin/{{pr_branch}}
24
24
  ```
25
25
 
26
- 2. For each changed file, verify:
26
+ 2. Think about deploy risk before commenting:
27
+ - What user-visible behavior changed?
28
+ - What dependencies, callers, or tests could be affected?
29
+ - What security, data-loss, concurrency, or compatibility risks are plausible for this diff?
30
+
31
+ 3. Run or inspect the repo's documented checks when practical. Use `CLAUDE.md`, README, package scripts, Makefile, or equivalent as the command source of truth, and record the exact commands/results in the review body.
32
+
33
+ 4. For each changed file, verify:
27
34
  - Does it follow existing patterns?
28
35
  - Are file paths and imports correct?
29
36
  - Follows the project's logging conventions (check CLAUDE.md)?
@@ -31,14 +38,19 @@ Use subagents only for genuinely parallel, independent tasks (e.g., reviewing un
31
38
  - Tests cover the important logic?
32
39
  - No security issues (injection, unsanitized input)?
33
40
 
34
- 3. Do NOT blindly approve. If you find real issues:
41
+ 5. Classify findings by ship risk:
42
+ - **Blocking:** failing checks, security/data-loss risk, broken existing behavior, missing requested behavior, invalid API/schema/data migration, or tests that do not cover changed critical logic.
43
+ - **Non-blocking:** style preferences, minor refactors, optional documentation, low-risk performance ideas, or additional tests that are useful but not required for safety.
44
+
45
+ 6. Do NOT blindly approve. If you find real blocking issues:
35
46
  - Verdict: **REQUEST_CHANGES**
36
47
  - List specific issues with file paths and line numbers
37
48
  - Describe what needs to change
38
49
 
39
- 4. If the code is genuinely ready:
50
+ 7. If the code is genuinely ready:
40
51
  - Verdict: **APPROVE**
41
52
  - Note any minor non-blocking suggestions
53
+ - Do not request changes for nits, speculative edge cases, or unrelated improvements
42
54
 
43
55
  ## Post Review — Submit your verdict
44
56
 
@@ -55,6 +67,19 @@ Your review body **MUST** start with one of these verdict lines (exactly as show
55
67
  Follow the verdict line with your detailed review findings, then sign off:
56
68
  - Sign: `Review by Minions ({{agent_name}} — {{agent_role}})`
57
69
 
70
+ Use this structure after the verdict:
71
+
72
+ ```markdown
73
+ Automated checks:
74
+ - `<command>`: pass/fail/skipped — short result or reason
75
+
76
+ Blocking issues:
77
+ - None, or `path:line` — issue and required fix
78
+
79
+ Non-blocking suggestions:
80
+ - None, or `path:line` — suggestion
81
+ ```
82
+
58
83
  After running the command, confirm it succeeded (check the command output for errors). If it fails, retry once.
59
84
 
60
85
  ## Handling Merge Conflicts
@@ -13,6 +13,8 @@ Treat a Minions assignment like the user typed the same task directly into a cap
13
13
  - Optimize for the requested outcome, not for mechanically completing checklist steps.
14
14
  - Use judgment to choose the smallest reliable workflow that fully satisfies the task.
15
15
  - Read only the context needed to make correct decisions; do not perform broad archaeology unless the task requires it.
16
+ - Build an initial context pack before editing: start from repo docs and team memory, identify 5-15 candidate files by path names, symbols, imports, tests, and comparable implementations, then read the smallest useful pack (usually 5-8 files). Expand beyond that only when a specific gap or failure proves more context is needed.
17
+ - Prefer dependency-aware context over keyword-only searching: when touching a file, also check its direct imports, direct callers when easy to find, and corresponding tests. For small repos, a simple repo map plus targeted search is enough.
16
18
  - Validate with the repo's own documented commands and acceptance criteria. If full validation is impossible or pre-existing failures block it, explain that precisely instead of inventing a green result.
17
19
  - Prefer direct work over ceremony. Branches, PRs, inbox notes, completion reports/blocks, and status comments exist for traceability; they should not change what "done" means for the user.
18
20
  - Safety and observability rules still win: stay in the engine-created worktree, do not self-merge, do not edit engine-managed status files, do not hide failures, and leave enough evidence for the human and engine to track the result.