@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 +10 -0
- package/dashboard.js +61 -44
- package/docs/plan-lifecycle.md +23 -7
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +81 -158
- package/engine/shared.js +28 -0
- package/engine.js +50 -31
- package/package.json +1 -1
- package/playbooks/fix.md +9 -0
- package/playbooks/implement.md +11 -0
- package/playbooks/plan.md +2 -0
- package/playbooks/review.md +28 -3
- package/playbooks/shared-rules.md +2 -0
package/CHANGELOG.md
CHANGED
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
|
|
2313
|
-
if (!
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
-
|
|
2351
|
-
|
|
2352
|
-
planData
|
|
2353
|
-
|
|
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
|
-
|
|
3576
|
-
const
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
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
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
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 =
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
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 =
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
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
|
|
3969
|
-
|
|
3970
|
-
|
|
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
|
-
|
|
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 =
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
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');
|
package/docs/plan-lifecycle.md
CHANGED
|
@@ -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
|
|
10
|
-
→
|
|
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. **
|
|
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/*.
|
|
134
|
-
| `
|
|
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`,
|
|
155
|
+
| `engine/lifecycle.js` | `checkPlanCompletion`, completion hooks, PRD sync |
|
|
139
156
|
| `engine.js` | `spawnAgent` (dependency merging), `resolveDependencyBranches` |
|
|
140
|
-
|
package/engine/lifecycle.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
326
|
+
if (createdVerify) {
|
|
327
|
+
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
295
328
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
|
1855
|
-
|
|
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
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
|
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.
|
|
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
|
|
package/playbooks/implement.md
CHANGED
|
@@ -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
|
package/playbooks/review.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|