@yemi33/minions 0.1.1999 → 0.1.2000
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/dashboard/js/render-prs.js +23 -1
- package/dashboard/js/render-work-items.js +11 -1
- package/dashboard.js +99 -1
- package/docs/completion-reports.md +33 -0
- package/docs/pr-comment-followup.md +206 -0
- package/engine/lifecycle.js +80 -0
- package/package.json +1 -1
- package/playbooks/fix.md +16 -1
- package/playbooks/review.md +14 -0
- package/playbooks/shared-rules.md +21 -0
- package/playbooks/templates/followup-dispatch.md +157 -0
|
@@ -4,6 +4,24 @@ let allPrs = [];
|
|
|
4
4
|
let prPage = 0;
|
|
5
5
|
const PR_PER_PAGE = 25;
|
|
6
6
|
|
|
7
|
+
function _countPrFollowups(pr) {
|
|
8
|
+
// PR follow-up chip (W-mpej3cox00099466) — counts WIs whose
|
|
9
|
+
// meta.pr_followup.parent_pr_url or parent_pr_id matches this PR.
|
|
10
|
+
if (!pr) return 0;
|
|
11
|
+
var wis = (window._lastWorkItems) || [];
|
|
12
|
+
if (!wis.length) return 0;
|
|
13
|
+
var prUrl = pr.url || '';
|
|
14
|
+
var prId = pr.id || '';
|
|
15
|
+
var n = 0;
|
|
16
|
+
for (var i = 0; i < wis.length; i++) {
|
|
17
|
+
var f = wis[i] && wis[i].meta && wis[i].meta.pr_followup;
|
|
18
|
+
if (!f) continue;
|
|
19
|
+
if (prUrl && f.parent_pr_url === prUrl) { n++; continue; }
|
|
20
|
+
if (prId && f.parent_pr_id === prId) { n++; }
|
|
21
|
+
}
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
function prRow(pr) {
|
|
8
26
|
// Minions review (agent) state — separate from ADO human review
|
|
9
27
|
const sq = pr.minionsReview || {};
|
|
@@ -27,9 +45,13 @@ function prRow(pr) {
|
|
|
27
45
|
const pendingReasonHtml = pendingReason
|
|
28
46
|
? '<div style="font-size:9px;color:var(--muted);margin-top:2px" title="Pending reason: ' + escapeHtml(pendingReason) + '">' + escapeHtml(pendingReason.replace(/_/g, ' ')) + '</div>'
|
|
29
47
|
: '';
|
|
48
|
+
var followupCount = _countPrFollowups(pr);
|
|
49
|
+
var followupChip = followupCount > 0
|
|
50
|
+
? ' <span class="pr-badge draft" style="font-size:8px" title="' + followupCount + ' follow-up work item(s) dispatched from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
|
|
51
|
+
: '';
|
|
30
52
|
return '<tr>' +
|
|
31
53
|
'<td><span class="pr-id">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
32
|
-
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
54
|
+
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + followupChip + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
33
55
|
'<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
|
|
34
56
|
'<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
35
57
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
@@ -40,9 +40,19 @@ function wiRow(item) {
|
|
|
40
40
|
: (item.branchStrategy === 'shared-branch' && item.status === 'done')
|
|
41
41
|
? '<span style="font-size:9px;color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
|
|
42
42
|
: '<span style="color:var(--muted)">—</span>';
|
|
43
|
+
// PR follow-up chip (W-mpej3cox00099466) — surfaced when the WI was spun
|
|
44
|
+
// off from a PR comment via meta.pr_followup. Links to the parent PR.
|
|
45
|
+
var prFollowup = item.meta && item.meta.pr_followup;
|
|
46
|
+
var followupChip = '';
|
|
47
|
+
if (prFollowup && prFollowup.parent_pr_url) {
|
|
48
|
+
var prRef = prFollowup.parent_pr_id || prFollowup.parent_pr_url;
|
|
49
|
+
var prNumMatch = String(prRef).match(/(\d+)(?!.*\d)/);
|
|
50
|
+
var prLabel = prNumMatch ? ('PR #' + prNumMatch[1]) : 'parent PR';
|
|
51
|
+
followupChip = ' <a class="pr-badge draft" style="font-size:8px;text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (prFollowup.parent_comment_author ? ' by ' + escapeHtml(prFollowup.parent_comment_author) : '') + '" onclick="event.stopPropagation()">↩ from ' + escapeHtml(prLabel) + '</a>';
|
|
52
|
+
}
|
|
43
53
|
return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
|
|
44
54
|
'<td><span class="pr-id">' + escapeHtml(item.id || '') + '</span></td>' +
|
|
45
|
-
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + '</td>' +
|
|
55
|
+
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
|
|
46
56
|
'<td><span style="font-size:10px;color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
|
|
47
57
|
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:8px">fan-out</span>' : '') + '</td>' +
|
|
48
58
|
'<td>' + typeBadge(item.type) + '</td>' +
|
package/dashboard.js
CHANGED
|
@@ -281,6 +281,62 @@ function copyWorkItemPrFields(item, input, pr = null) {
|
|
|
281
281
|
if (pr?.url || input.prUrl) item.prUrl = pr?.url || input.prUrl;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// ─── PR follow-up dispatch (W-mpej3cox00099466) ─────────────────────────────
|
|
285
|
+
// Validates the meta.pr_followup shape and enforces parent_comment_id-keyed
|
|
286
|
+
// dedup across the project's work-items so retries from the fix/review agent
|
|
287
|
+
// (or two pollers racing) don't fan out duplicate follow-up WIs. See
|
|
288
|
+
// playbooks/templates/followup-dispatch.md and docs/pr-comment-followup.md.
|
|
289
|
+
|
|
290
|
+
const PR_FOLLOWUP_REQUIRED_FIELDS = ['parent_pr_url', 'parent_pr_id', 'parent_comment_id', 'parent_comment_author'];
|
|
291
|
+
const PR_FOLLOWUP_MAX_FIELD_LEN = 512;
|
|
292
|
+
|
|
293
|
+
function validatePrFollowupShape(value) {
|
|
294
|
+
if (value === undefined || value === null) return { valid: true, value: null };
|
|
295
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
296
|
+
return { valid: false, error: 'meta.pr_followup must be an object with parent_pr_url, parent_pr_id, parent_comment_id, parent_comment_author (strings)' };
|
|
297
|
+
}
|
|
298
|
+
const cleaned = {};
|
|
299
|
+
for (const field of PR_FOLLOWUP_REQUIRED_FIELDS) {
|
|
300
|
+
const raw = value[field];
|
|
301
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
302
|
+
return { valid: false, error: `meta.pr_followup.${field} is required and must be a non-empty string` };
|
|
303
|
+
}
|
|
304
|
+
if (raw.length > PR_FOLLOWUP_MAX_FIELD_LEN) {
|
|
305
|
+
return { valid: false, error: `meta.pr_followup.${field} exceeds ${PR_FOLLOWUP_MAX_FIELD_LEN} characters` };
|
|
306
|
+
}
|
|
307
|
+
cleaned[field] = raw.trim();
|
|
308
|
+
}
|
|
309
|
+
return { valid: true, value: cleaned };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function findExistingFollowupForComment(items, parentCommentId) {
|
|
313
|
+
if (!Array.isArray(items) || !parentCommentId) return null;
|
|
314
|
+
const needle = String(parentCommentId).trim();
|
|
315
|
+
if (!needle) return null;
|
|
316
|
+
return items.find(item => {
|
|
317
|
+
const seen = item?.meta?.pr_followup?.parent_comment_id;
|
|
318
|
+
return typeof seen === 'string' && seen.trim() === needle;
|
|
319
|
+
}) || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function extractMinionsAgentHeader(req) {
|
|
323
|
+
const raw = req?.headers?.['x-minions-agent'];
|
|
324
|
+
if (typeof raw !== 'string') return '';
|
|
325
|
+
const trimmed = raw.trim();
|
|
326
|
+
if (!trimmed || trimmed.length > 128) return '';
|
|
327
|
+
// Restrict to safe characters; agent ids are kebab-case or `temp-…`.
|
|
328
|
+
return /^[A-Za-z0-9._:-]+$/.test(trimmed) ? trimmed : '';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function extractMinionsOriginWiHeader(req) {
|
|
332
|
+
const raw = req?.headers?.['x-minions-origin-wi'];
|
|
333
|
+
if (typeof raw !== 'string') return '';
|
|
334
|
+
const trimmed = raw.trim();
|
|
335
|
+
if (!trimmed || trimmed.length > 128) return '';
|
|
336
|
+
return /^[A-Za-z0-9._:-]+$/.test(trimmed) ? trimmed : '';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
284
340
|
function normalizeWorkItemDedupText(value) {
|
|
285
341
|
return String(value == null ? '' : value)
|
|
286
342
|
.replace(/\r\n/g, '\n')
|
|
@@ -4681,9 +4737,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
4681
4737
|
// meta.keep_processes documentation in prompts/cc-system.md and
|
|
4682
4738
|
// the documented `meta?` /api/routes parameter would silently no-op.
|
|
4683
4739
|
// Shallow copy of plain objects only — arrays/null/primitives are dropped.
|
|
4740
|
+
let validatedFollowup = null;
|
|
4684
4741
|
if (body.meta && typeof body.meta === 'object' && !Array.isArray(body.meta)) {
|
|
4742
|
+
// PR follow-up validation (W-mpej3cox00099466) — verify meta.pr_followup
|
|
4743
|
+
// shape BEFORE the WI is written so a malformed sidecar can't slip in.
|
|
4744
|
+
const followupCheck = validatePrFollowupShape(body.meta.pr_followup);
|
|
4745
|
+
if (!followupCheck.valid) {
|
|
4746
|
+
return jsonReply(res, 400, { error: followupCheck.error });
|
|
4747
|
+
}
|
|
4685
4748
|
item.meta = { ...body.meta };
|
|
4749
|
+
if (followupCheck.value) {
|
|
4750
|
+
item.meta.pr_followup = followupCheck.value;
|
|
4751
|
+
validatedFollowup = followupCheck.value;
|
|
4752
|
+
}
|
|
4686
4753
|
}
|
|
4754
|
+
// PR follow-up traceability headers (W-mpej3cox00099466).
|
|
4755
|
+
const originAgent = extractMinionsAgentHeader(req);
|
|
4756
|
+
const originWi = extractMinionsOriginWiHeader(req);
|
|
4757
|
+
if (originAgent) item._originAgent = originAgent;
|
|
4758
|
+
if (originWi) item._originWi = originWi;
|
|
4687
4759
|
copyWorkItemPrFields(item, body);
|
|
4688
4760
|
// W-mpejf0fq000e84d6: pre-compute the canonical branch name at create
|
|
4689
4761
|
// time so the persisted WI carries `branch` from the moment it hits
|
|
@@ -4698,6 +4770,28 @@ const server = http.createServer(async (req, res) => {
|
|
|
4698
4770
|
if (derived) item.branch = derived;
|
|
4699
4771
|
} catch (e) { /* identity resolver best-effort; engine will derive on dispatch */ }
|
|
4700
4772
|
}
|
|
4773
|
+
// PR follow-up parent_comment_id dedup (W-mpej3cox00099466) — must run
|
|
4774
|
+
// inside the same lock as createWorkItemWithDedup so a racing pair of
|
|
4775
|
+
// pollers/agents can't both win. Standard title/type/project dedup runs
|
|
4776
|
+
// afterward; the follow-up check is stricter (project-wide, all
|
|
4777
|
+
// statuses) and takes precedence when matched.
|
|
4778
|
+
if (validatedFollowup) {
|
|
4779
|
+
let followupConflict = null;
|
|
4780
|
+
mutateWorkItems(wiPath, items => {
|
|
4781
|
+
followupConflict = findExistingFollowupForComment(items, validatedFollowup.parent_comment_id);
|
|
4782
|
+
if (followupConflict) return items;
|
|
4783
|
+
items.push(item);
|
|
4784
|
+
return items;
|
|
4785
|
+
});
|
|
4786
|
+
if (followupConflict) {
|
|
4787
|
+
return jsonReply(res, 409, {
|
|
4788
|
+
error: 'followup_already_dispatched',
|
|
4789
|
+
existingWiId: followupConflict.id,
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4792
|
+
recordCcTurnIfPresent(req, { kind: 'work-item', id, title: item.title, project: item.project || null });
|
|
4793
|
+
return jsonReply(res, 200, { ok: true, id });
|
|
4794
|
+
}
|
|
4701
4795
|
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
4702
4796
|
if (!createResult.created) {
|
|
4703
4797
|
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
@@ -9112,7 +9206,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9112
9206
|
{ method: 'DELETE', path: /^\/api\/qa\/runbooks\/([^/?]+)$/, template: '/api/qa/runbooks/<id>', desc: 'Delete a QA runbook by id.', handler: handleQaRunbooksDelete },
|
|
9113
9207
|
|
|
9114
9208
|
// Work items
|
|
9115
|
-
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?, oneShot?, meta?', handler: handleWorkItemsCreate },
|
|
9209
|
+
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?, oneShot?, meta?, meta.pr_followup?, X-Minions-Agent?, X-Minions-Origin-Wi?', handler: handleWorkItemsCreate },
|
|
9116
9210
|
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?', handler: handleWorkItemsUpdate },
|
|
9117
9211
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
9118
9212
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
@@ -9925,6 +10019,10 @@ module.exports = {
|
|
|
9925
10019
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
9926
10020
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
9927
10021
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
10022
|
+
_validatePrFollowupShape: validatePrFollowupShape,
|
|
10023
|
+
_findExistingFollowupForComment: findExistingFollowupForComment,
|
|
10024
|
+
_extractMinionsAgentHeader: extractMinionsAgentHeader,
|
|
10025
|
+
_extractMinionsOriginWiHeader: extractMinionsOriginWiHeader,
|
|
9928
10026
|
_buildPlanWorkItem: buildPlanWorkItem,
|
|
9929
10027
|
_buildManualPrdItemPlan: buildManualPrdItemPlan,
|
|
9930
10028
|
_resolveScheduleProjectValue: resolveScheduleProjectValue,
|
|
@@ -83,6 +83,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
83
83
|
| `files_changed` | string \| array | Comma-separated list (or array) of key files changed. |
|
|
84
84
|
| `tests` | string | `pass`, `fail`, `skipped`, `N/A`, or a free-form note like `skipped — relying on PR pipeline`. |
|
|
85
85
|
| `pending` | string | Any remaining work, or `none`. |
|
|
86
|
+
| `followups` | array | Optional. PR-comment follow-up work items the agent dispatched via `POST /api/work-items` with `meta.pr_followup` set. Each entry: `{wi_id, title, reason, parent_comment_id}`. See [PR-comment follow-ups](#pr-comment-follow-ups). |
|
|
86
87
|
|
|
87
88
|
## `failure_class` enum
|
|
88
89
|
|
|
@@ -129,6 +130,38 @@ Engine behavior when `noop: true` (`engine/lifecycle.js` `parseCompletionNoop` +
|
|
|
129
130
|
|
|
130
131
|
Without `noop: true`, an empty PR field will be flagged as a missing-PR-attachment failure and auto-retried up to `ENGINE_DEFAULTS.maxRetries` times.
|
|
131
132
|
|
|
133
|
+
## PR-comment follow-ups
|
|
134
|
+
|
|
135
|
+
Fix and review agents can spin off new Minions work items in response to PR
|
|
136
|
+
comments that request **legitimate but out-of-scope** work. See
|
|
137
|
+
`playbooks/templates/followup-dispatch.md` for the full contract (decision
|
|
138
|
+
tree, exact `POST /api/work-items` shape, and required `meta.pr_followup`
|
|
139
|
+
fields). When the agent does so, it MUST record the dispatched WIs in the
|
|
140
|
+
optional `followups` array on its completion report:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"status": "success",
|
|
145
|
+
"summary": "Fixed reviewer's blocking finding on PR #2400; dispatched W-mp9abcdef as out-of-scope follow-up.",
|
|
146
|
+
"pr": "https://github.com/yemi33/minions/pull/2400",
|
|
147
|
+
"followups": [
|
|
148
|
+
{
|
|
149
|
+
"wi_id": "W-mp9abcdef",
|
|
150
|
+
"title": "Extract markdown sanitizer into shared module",
|
|
151
|
+
"reason": "Reviewer asked to track as separate WI (out of scope for #2400 — see comment 4567890)",
|
|
152
|
+
"parent_comment_id": "4567890"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Engine behavior (`engine/lifecycle.js` `processCompletionFollowups`):
|
|
159
|
+
|
|
160
|
+
- Each entry is logged at `info`: `Followup audit (<agent> wi=<parent>): dispatched <wi_id> — <title> (comment=<id>) — <reason>`.
|
|
161
|
+
- If the claimed `wi_id` does not exist in any project's `work-items.json` at parse time, a `warn` is emitted. This catches dispatches that were deduped to a 409 / 200-duplicate response, reverted, or never actually landed.
|
|
162
|
+
- The `followups` array is **descriptive only** — the engine does not auto-create or auto-link these WIs from the report. Creation happens at the moment the agent calls `POST /api/work-items`; the report is just the auditable record.
|
|
163
|
+
- Malformed entries (non-object, missing `wi_id`) are logged and skipped.
|
|
164
|
+
|
|
132
165
|
## `retryable` and `needs_rerun`
|
|
133
166
|
|
|
134
167
|
Both default to `false`. Together they let the agent override the engine's default retry policy:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# PR-Comment Follow-up Dispatch
|
|
2
|
+
|
|
3
|
+
Status: shipped in **W-mpej3cox00099466** (replaces the marker-based "Option A"
|
|
4
|
+
proposal investigated in CC turn `cct-mpeira1y0001ddff`).
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
`engine/{github,ado}.js` `pollPrHumanComments` routes every actionable human PR
|
|
9
|
+
comment into `pr.humanFeedback.pendingFix:true`, which `discover-from-prs`
|
|
10
|
+
turns into a `fix` dispatch on the **same** branch. The `fix.md` playbook then
|
|
11
|
+
forbids broadening the PR or creating a replacement branch. The net result was
|
|
12
|
+
a binary outcome for every comment:
|
|
13
|
+
|
|
14
|
+
1. **In-scope** → fix on the current branch, push, reply on the thread.
|
|
15
|
+
2. **Out of scope / invalid / harmful** → post a rebuttal, leave the thread
|
|
16
|
+
open, do not change code.
|
|
17
|
+
|
|
18
|
+
There was no third option for the common case: *"Yes, this is a real ask, but
|
|
19
|
+
it doesn't belong in this PR — track it as a separate WI."* Agents either
|
|
20
|
+
silently rebutted those comments (frustrating reviewers) or broadened the PR
|
|
21
|
+
(making it un-reviewable).
|
|
22
|
+
|
|
23
|
+
This is **Option B** from the CC investigation: agent judgment + a documented
|
|
24
|
+
playbook contract. The poller is unchanged; the carve-out is purely additive.
|
|
25
|
+
|
|
26
|
+
## End-to-end walkthrough
|
|
27
|
+
|
|
28
|
+
1. **Human leaves a comment on PR #2400** asking for a related-but-distinct
|
|
29
|
+
refactor: *"Can you also extract the markdown sanitizer into a shared module?
|
|
30
|
+
Probably worth a separate PR though."*
|
|
31
|
+
2. **Engine pollers** (`engine/github.js` `pollPrHumanComments`) flag the
|
|
32
|
+
comment, set `pr.humanFeedback.pendingFix:true`, and `discover-from-prs`
|
|
33
|
+
queues a `fix` dispatch against PR #2400's branch.
|
|
34
|
+
3. **Fix agent (e.g. lambert)** starts working. After fetching the diff and
|
|
35
|
+
the comment, it classifies each ask using the decision tree in
|
|
36
|
+
`playbooks/templates/followup-dispatch.md`:
|
|
37
|
+
- The in-scope comments → fix on this branch.
|
|
38
|
+
- The "extract sanitizer" comment → **follow-up**.
|
|
39
|
+
4. **Follow-up dispatch.** The agent calls the dashboard API with the four
|
|
40
|
+
`meta.pr_followup` fields and the two identification headers:
|
|
41
|
+
```bash
|
|
42
|
+
curl -sS -X POST http://localhost:7331/api/work-items \
|
|
43
|
+
-H 'Content-Type: application/json' \
|
|
44
|
+
-H 'X-Minions-Agent: lambert' \
|
|
45
|
+
-H 'X-Minions-Origin-Wi: W-mp7originalwi' \
|
|
46
|
+
-d '{
|
|
47
|
+
"title": "Extract markdown sanitizer into shared module",
|
|
48
|
+
"type": "implement",
|
|
49
|
+
"priority": "medium",
|
|
50
|
+
"project": "minions",
|
|
51
|
+
"description": "Reviewer asked to track separately from PR #2400…",
|
|
52
|
+
"references": [
|
|
53
|
+
{"url": "https://github.com/yemi33/minions/pull/2400", "label": "Originated from PR #2400 comment"}
|
|
54
|
+
],
|
|
55
|
+
"meta": {
|
|
56
|
+
"pr_followup": {
|
|
57
|
+
"parent_pr_url": "https://github.com/yemi33/minions/pull/2400",
|
|
58
|
+
"parent_pr_id": "github:yemi33/minions#2400",
|
|
59
|
+
"parent_comment_id": "4567890",
|
|
60
|
+
"parent_comment_author": "alice"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}'
|
|
64
|
+
```
|
|
65
|
+
5. **Server (`dashboard.js handleWorkItemsCreate`)**:
|
|
66
|
+
- Validates `meta.pr_followup` shape (all four fields required, ≤ 512
|
|
67
|
+
chars each). Malformed → `400 { error: "<message>" }`.
|
|
68
|
+
- Checks for an existing follow-up with the same `parent_comment_id` in
|
|
69
|
+
the target project's `work-items.json`. Duplicate → `409 {
|
|
70
|
+
error: "followup_already_dispatched", existingWiId: "W-…" }` (no second WI
|
|
71
|
+
created).
|
|
72
|
+
- Persists the validated `meta.pr_followup`, and stores
|
|
73
|
+
`X-Minions-Agent` / `X-Minions-Origin-Wi` as `_originAgent` /
|
|
74
|
+
`_originWi` on the WI for the dashboard timeline.
|
|
75
|
+
- Returns `200 { ok: true, id: "W-mp9abcdef" }`.
|
|
76
|
+
6. **Reply on the thread.** The agent posts a comment on the original PR
|
|
77
|
+
thread:
|
|
78
|
+
> Tracked as follow-up: **W-mp9abcdef** — see the Minions dashboard.
|
|
79
|
+
> Continuing with the in-place fix on this PR.
|
|
80
|
+
|
|
81
|
+
…and resolves that sub-thread. The rest of the PR review proceeds.
|
|
82
|
+
7. **Dashboard chips.**
|
|
83
|
+
- The new WI's row shows `↪ from PR #2400`, linking back to the parent PR.
|
|
84
|
+
(`dashboard/js/render-work-items.js` `prFollowup` block.)
|
|
85
|
+
- The PR row for #2400 shows `+1 follow-up`, computed live from
|
|
86
|
+
`window._lastWorkItems` and rendered in
|
|
87
|
+
`dashboard/js/render-prs.js _countPrFollowups`.
|
|
88
|
+
8. **Completion report.** When the fix agent finishes its in-place work and
|
|
89
|
+
writes the completion JSON, it includes a `followups` array:
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"status": "success",
|
|
93
|
+
"summary": "Fixed reviewer's blocking finding; dispatched W-mp9abcdef as out-of-scope follow-up.",
|
|
94
|
+
"pr": "https://github.com/yemi33/minions/pull/2400",
|
|
95
|
+
"followups": [
|
|
96
|
+
{
|
|
97
|
+
"wi_id": "W-mp9abcdef",
|
|
98
|
+
"title": "Extract markdown sanitizer into shared module",
|
|
99
|
+
"reason": "Out-of-scope ask from reviewer on PR #2400 comment #4567890",
|
|
100
|
+
"parent_comment_id": "4567890"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
`engine/lifecycle.js processCompletionFollowups` logs each entry at `info`
|
|
106
|
+
and warns if the claimed `wi_id` is missing from any project's
|
|
107
|
+
`work-items.json` (catches reverted or non-landing dispatches).
|
|
108
|
+
|
|
109
|
+
## Server contract reference
|
|
110
|
+
|
|
111
|
+
**Headers** (`dashboard.js handleWorkItemsCreate`):
|
|
112
|
+
|
|
113
|
+
| Header | Type | Persisted as | Notes |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| `X-Minions-Agent` | `^[A-Za-z0-9._:-]{1,128}$` | `_originAgent` | Agent id (`lambert`, `temp-…`). Both headers are optional but recommended for traceability. |
|
|
116
|
+
| `X-Minions-Origin-Wi` | `^[A-Za-z0-9._:-]{1,128}$` | `_originWi` | The dispatch's parent WI id. |
|
|
117
|
+
|
|
118
|
+
**`meta.pr_followup` shape** (validated by `validatePrFollowupShape` in
|
|
119
|
+
`dashboard.js`):
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
{
|
|
123
|
+
parent_pr_url: string, // required, ≤ 512 chars
|
|
124
|
+
parent_pr_id: string, // required, ≤ 512 chars (canonical id, e.g. github:owner/repo#123 or PR-123)
|
|
125
|
+
parent_comment_id: string, // required, ≤ 512 chars (host comment/thread id; key for dedup)
|
|
126
|
+
parent_comment_author: string // required, ≤ 512 chars
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Any missing/empty field, oversized field, or non-string value → `400` with a
|
|
131
|
+
descriptive message.
|
|
132
|
+
|
|
133
|
+
**Dedup.** A second `POST /api/work-items` with the same
|
|
134
|
+
`meta.pr_followup.parent_comment_id` in the same project's `work-items.json`
|
|
135
|
+
returns `409 { error: "followup_already_dispatched", existingWiId: "<id>" }`.
|
|
136
|
+
This is checked under the same `mutateWorkItems` lock used to insert the new
|
|
137
|
+
WI, so two pollers racing on the same comment cannot both win.
|
|
138
|
+
|
|
139
|
+
Treat 409 as a **success** for the agent's purposes — the follow-up already
|
|
140
|
+
exists; you still reply on the comment thread with the returned id.
|
|
141
|
+
|
|
142
|
+
## What does NOT change
|
|
143
|
+
|
|
144
|
+
- The pollers (`engine/github.js`, `engine/ado.js`) still set
|
|
145
|
+
`pr.humanFeedback.pendingFix:true` for every actionable human comment.
|
|
146
|
+
- `discover-from-prs` still queues `fix` dispatches against the original
|
|
147
|
+
branch; nothing prevents the in-place fix from proceeding.
|
|
148
|
+
- Follow-up routing is not auto-pinned to any agent — `routing.md` resolves
|
|
149
|
+
the new WI to the appropriate agent based on `type` + `project`.
|
|
150
|
+
- The follow-up is **descriptive** in the completion report: the engine does
|
|
151
|
+
not auto-create the WI from the report. Creation happens when the agent
|
|
152
|
+
calls the API; the `followups` array is the auditable record.
|
|
153
|
+
|
|
154
|
+
## When to use this carve-out (and when not)
|
|
155
|
+
|
|
156
|
+
**Use it when:**
|
|
157
|
+
|
|
158
|
+
- The reviewer explicitly asks to "track separately", "do in a follow-up", or
|
|
159
|
+
"open a new PR".
|
|
160
|
+
- The ask is in a different file/system/feature area than the current diff.
|
|
161
|
+
- Adding the change to this PR would more than double the review surface.
|
|
162
|
+
|
|
163
|
+
**Do NOT use it for:**
|
|
164
|
+
|
|
165
|
+
- Nits, style notes, or things you were already going to do in this PR.
|
|
166
|
+
- Asks that block correctness of the current PR (those are the original
|
|
167
|
+
"fix it" path).
|
|
168
|
+
- Asks that are invalid, stale, or harmful (those are the "rebut with
|
|
169
|
+
rationale" path — no follow-up).
|
|
170
|
+
|
|
171
|
+
## Where the wiring lives
|
|
172
|
+
|
|
173
|
+
| File | Role |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `playbooks/templates/followup-dispatch.md` | Canonical decision tree + curl shape + dedup contract. Referenced from `fix.md` and `review.md`. |
|
|
176
|
+
| `playbooks/fix.md` (`## Follow-up Dispatch`) | Carve-out for fix agents; updated decision tree splitting "fix it / rebut it / spin off". |
|
|
177
|
+
| `playbooks/review.md` (`## Follow-up Dispatch`) | Same carve-out for review agents who spot trackable out-of-scope work. |
|
|
178
|
+
| `playbooks/shared-rules.md` (`## Minions API access`) | Documents the dashboard URL, the route registry endpoint, and the `X-Minions-Agent` / `X-Minions-Origin-Wi` headers. |
|
|
179
|
+
| `dashboard.js` (`validatePrFollowupShape`, `findExistingFollowupForComment`, `extractMinionsAgentHeader`, `extractMinionsOriginWiHeader`, `handleWorkItemsCreate`) | Shape validation, parent_comment_id dedup, header persistence, 200/400/409 responses. |
|
|
180
|
+
| `dashboard/js/render-work-items.js` (`prFollowup` block in `wiRow`) | `↪ from PR #N` chip on follow-up WIs. |
|
|
181
|
+
| `dashboard/js/render-prs.js` (`_countPrFollowups`) | `+N follow-ups` chip on PR rows. |
|
|
182
|
+
| `engine/lifecycle.js` (`processCompletionFollowups`) | Parses the optional `followups` array on completion reports; warns on mismatch. |
|
|
183
|
+
| `docs/completion-reports.md` (`PR-comment follow-ups`) | Schema for the optional `followups` array. |
|
|
184
|
+
|
|
185
|
+
## Migration / rollout
|
|
186
|
+
|
|
187
|
+
This is purely additive. Existing fix/review playbooks continue to work
|
|
188
|
+
without setting `meta.pr_followup`; the validator runs only when the field is
|
|
189
|
+
present. Agents that don't know about the carve-out keep doing in-place fixes
|
|
190
|
+
and rebuttals exactly as before.
|
|
191
|
+
|
|
192
|
+
To adopt:
|
|
193
|
+
|
|
194
|
+
1. Update agent prompts (already done in this PR via `playbooks/fix.md` and
|
|
195
|
+
`playbooks/review.md`).
|
|
196
|
+
2. Agents read the contract in `playbooks/templates/followup-dispatch.md`
|
|
197
|
+
when they reach the `## Follow-up Dispatch` section of their playbook.
|
|
198
|
+
3. The next time a reviewer asks for an out-of-scope follow-up, the fix
|
|
199
|
+
agent fans out a new WI instead of refusing or broadening the PR.
|
|
200
|
+
|
|
201
|
+
## Related
|
|
202
|
+
|
|
203
|
+
- `engine/github.js pollPrHumanComments` (human-comment routing — unchanged)
|
|
204
|
+
- `engine.js HUMAN_FEEDBACK fix dispatch` (in-place fix flow — unchanged)
|
|
205
|
+
- `playbooks/templates/followup-dispatch.md` (the contract agents read)
|
|
206
|
+
- `docs/completion-reports.md` `PR-comment follow-ups` section
|
package/engine/lifecycle.js
CHANGED
|
@@ -3360,6 +3360,74 @@ function extractDecompositionJson(stdout, runtimeName) {
|
|
|
3360
3360
|
return null;
|
|
3361
3361
|
}
|
|
3362
3362
|
|
|
3363
|
+
/**
|
|
3364
|
+
* PR follow-up audit (W-mpej3cox00099466).
|
|
3365
|
+
*
|
|
3366
|
+
* Parses the optional `followups` array on the agent's completion report,
|
|
3367
|
+
* logs each entry at `info` so operators can trace fan-out, and warns when
|
|
3368
|
+
* a claimed `wi_id` doesn't match any record in central or per-project
|
|
3369
|
+
* work-items.json. The mismatch warning catches dispatches that were reverted
|
|
3370
|
+
* (HTTP 409 from /api/work-items dedup), fingerprinted away by the standard
|
|
3371
|
+
* dedup window, or otherwise didn't actually land — situations where the
|
|
3372
|
+
* agent claimed a follow-up exists but the dashboard/engine has no record.
|
|
3373
|
+
*
|
|
3374
|
+
* Returns the array of well-formed followup entries; malformed entries (non-
|
|
3375
|
+
* object, missing `wi_id`) are logged and skipped. Failures inside the
|
|
3376
|
+
* existence check are non-fatal — they degrade to a warn.
|
|
3377
|
+
*/
|
|
3378
|
+
function processCompletionFollowups(completion, agentId, dispatchItem, config) {
|
|
3379
|
+
if (!completion || typeof completion !== 'object') return [];
|
|
3380
|
+
const raw = completion.followups;
|
|
3381
|
+
if (raw === undefined || raw === null) return [];
|
|
3382
|
+
if (!Array.isArray(raw)) {
|
|
3383
|
+
log('warn', `Followup audit: completion.followups must be an array; got ${typeof raw}`);
|
|
3384
|
+
return [];
|
|
3385
|
+
}
|
|
3386
|
+
if (raw.length === 0) return [];
|
|
3387
|
+
const wiId = dispatchItem?.meta?.item?.id || 'N/A';
|
|
3388
|
+
let existingIds = null;
|
|
3389
|
+
function loadExistingIds() {
|
|
3390
|
+
if (existingIds !== null) return existingIds;
|
|
3391
|
+
try {
|
|
3392
|
+
existingIds = new Set(queries.getWorkItems(config).map(w => w && w.id).filter(Boolean));
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
log('warn', `Followup audit: getWorkItems failed (${err.message}); skipping existence checks`);
|
|
3395
|
+
existingIds = new Set();
|
|
3396
|
+
}
|
|
3397
|
+
return existingIds;
|
|
3398
|
+
}
|
|
3399
|
+
const accepted = [];
|
|
3400
|
+
for (const entry of raw) {
|
|
3401
|
+
if (!entry || typeof entry !== 'object') {
|
|
3402
|
+
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): skipping malformed entry (not an object)`);
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
const followupWiId = typeof entry.wi_id === 'string' ? entry.wi_id.trim()
|
|
3406
|
+
: (typeof entry.wiId === 'string' ? entry.wiId.trim() : '');
|
|
3407
|
+
if (!followupWiId) {
|
|
3408
|
+
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): skipping entry without wi_id`);
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
const title = typeof entry.title === 'string' ? entry.title.trim() : '';
|
|
3412
|
+
const reason = typeof entry.reason === 'string' ? entry.reason.trim() : '';
|
|
3413
|
+
const parentCommentId = typeof entry.parent_comment_id === 'string'
|
|
3414
|
+
? entry.parent_comment_id.trim()
|
|
3415
|
+
: (typeof entry.parentCommentId === 'string' ? entry.parentCommentId.trim() : '');
|
|
3416
|
+
log('info', `Followup audit (${agentId || 'unknown'} wi=${wiId}): dispatched ${followupWiId}${title ? ` — ${title}` : ''}${parentCommentId ? ` (comment=${parentCommentId})` : ''}${reason ? ` — ${reason}` : ''}`);
|
|
3417
|
+
const allIds = loadExistingIds();
|
|
3418
|
+
if (allIds.size > 0 && !allIds.has(followupWiId)) {
|
|
3419
|
+
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): claimed followup ${followupWiId} not found in any work-items.json — dispatch may have been deduped, reverted, or never landed`);
|
|
3420
|
+
}
|
|
3421
|
+
accepted.push({
|
|
3422
|
+
wi_id: followupWiId,
|
|
3423
|
+
title,
|
|
3424
|
+
reason,
|
|
3425
|
+
parent_comment_id: parentCommentId,
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
return accepted;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3363
3431
|
/**
|
|
3364
3432
|
* Handle decomposition result — parse sub-items from agent output and create child work items.
|
|
3365
3433
|
* Called from runPostCompletionHooks when type === 'decompose'.
|
|
@@ -3538,6 +3606,17 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
3538
3606
|
if (structuredCompletion.summary) resultSummary = String(structuredCompletion.summary);
|
|
3539
3607
|
log('info', `Structured completion from ${agentId}: status=${structuredCompletion.status}, pr=${structuredCompletion.pr || 'N/A'}${structuredCompletion._source ? ` (${structuredCompletion._source})` : ''}`);
|
|
3540
3608
|
}
|
|
3609
|
+
// PR follow-up audit (W-mpej3cox00099466) — when the agent declares
|
|
3610
|
+
// `followups: [{wi_id, title, reason, parent_comment_id}]` in its completion
|
|
3611
|
+
// report, log each entry and verify the claimed WI ids actually landed in
|
|
3612
|
+
// some project's work-items.json. A mismatch (claimed wi_id with no matching
|
|
3613
|
+
// record) is logged at warn so operators can spot dispatches that were
|
|
3614
|
+
// reverted, fingerprinted-deduped, or otherwise didn't take.
|
|
3615
|
+
if (structuredCompletion) {
|
|
3616
|
+
try {
|
|
3617
|
+
processCompletionFollowups(structuredCompletion, agentId, dispatchItem, config);
|
|
3618
|
+
} catch (err) { log('warn', `Followup audit: ${err.message}`); }
|
|
3619
|
+
}
|
|
3541
3620
|
// F5 (W-mpeklod3000we69c): if the agent flagged an injection attempt in the
|
|
3542
3621
|
// structured completion, force the dispatch into a non-retryable failure
|
|
3543
3622
|
// with `FAILURE_CLASS.INJECTION_FLAGGED`. Inbox note + WI stamp are written
|
|
@@ -4303,4 +4382,5 @@ module.exports = {
|
|
|
4303
4382
|
isPrAttachmentRequired,
|
|
4304
4383
|
extractDecompositionJson,
|
|
4305
4384
|
handleDecompositionResult,
|
|
4385
|
+
processCompletionFollowups,
|
|
4306
4386
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2000",
|
|
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
|
@@ -35,6 +35,7 @@ Before editing, split the feedback into:
|
|
|
35
35
|
|
|
36
36
|
- **Blocking findings to fix:** verified correctness, safety, build/test failure, missing requested behavior, broken compatibility, or approval-blocking comments whose claim is valid on the current branch.
|
|
37
37
|
- **Findings to answer with rationale:** comments where the current approach is intentionally correct, the reviewer misunderstood the code, the issue is stale/already addressed, or the requested change would broaden the PR beyond its purpose.
|
|
38
|
+
- **Out-of-scope but legitimate follow-up requests:** asks for genuinely useful work that does **not** belong in this PR (a separate bug, a related-but-distinct feature, a refactor the reviewer explicitly said to "track separately"). Spin off a new Minions work item — see `## Follow-up Dispatch` below — and reply on the originating thread with the new WI id. This is a third option alongside "fix it" and "rebut it"; it does **not** loosen the don't-broaden-the-PR rule for in-PR work.
|
|
38
39
|
- **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 verified blocking issue.
|
|
39
40
|
|
|
40
41
|
## Health Check
|
|
@@ -55,7 +56,7 @@ Handle this like the PR author responding directly from a CLI:
|
|
|
55
56
|
- Fix it if the feedback is valid and improves correctness, safety, maintainability, or test coverage.
|
|
56
57
|
- If the current approach is intentionally correct, stale, already fixed, out of scope, or the requested change would be harmful, reply with specific rationale instead of silently changing code or ignoring the thread.
|
|
57
58
|
- Handle merge conflicts when needed, preserving the PR's intended changes while keeping the branch reviewable.
|
|
58
|
-
- Do not add unrelated cleanups or broaden the PR beyond the review feedback unless that is necessary to make the fix correct.
|
|
59
|
+
- Do not add unrelated cleanups or broaden the PR beyond the review feedback unless that is necessary to make the fix correct. For legitimate-but-out-of-scope asks, see `## Follow-up Dispatch` below — spin off a separate WI instead of broadening this PR.
|
|
59
60
|
|
|
60
61
|
## Validation
|
|
61
62
|
|
|
@@ -94,6 +95,20 @@ After pushing, respond to each review comment/thread:
|
|
|
94
95
|
- **GitHub**: Reply to each review comment, resolve conversations you've fixed
|
|
95
96
|
- **ADO**: Use `az` CLI first to reply to each thread and update status when supported; use ADO MCP only as a fallback when `az` is unavailable or insufficient. Set status to `fixed` or `closed` for fixes; leave `active` for rationale replies
|
|
96
97
|
|
|
98
|
+
## Follow-up Dispatch
|
|
99
|
+
|
|
100
|
+
When a comment asks for legitimate work that is **clearly out of scope** for this PR (separate bug, different feature, refactor the reviewer explicitly asked to "track separately"), spin off a new Minions work item via the dashboard API instead of broadening the PR or silently declining. Read the full contract — decision tree, exact curl shape, required `meta.pr_followup` fields, dedup behavior, and the mandatory comment-thread reply — at:
|
|
101
|
+
|
|
102
|
+
`{{team_root}}/playbooks/templates/followup-dispatch.md`
|
|
103
|
+
|
|
104
|
+
Key rules (full detail in the template):
|
|
105
|
+
|
|
106
|
+
- The follow-up is **additive**. You still fix the in-scope feedback in this PR and post the normal fix comment.
|
|
107
|
+
- Reply on the originating comment thread with the new WI id (e.g. `Tracked as follow-up: W-…`) and resolve that sub-thread.
|
|
108
|
+
- Always include `meta.pr_followup.parent_comment_id` — without it the server cannot dedup retries.
|
|
109
|
+
- Do not pick `agent`; let `routing.md` decide.
|
|
110
|
+
- Record every follow-up you dispatched in the `followups` array of your completion JSON.
|
|
111
|
+
|
|
97
112
|
## When to Stop
|
|
98
113
|
|
|
99
114
|
Your task is complete when each review finding has either been fixed or answered with rationale, the validation story is truthful and sufficient for review, the fix is pushed if code changed, the PR is commented, and addressed threads are resolved. Do NOT continue into unrelated improvements.
|
package/playbooks/review.md
CHANGED
|
@@ -98,6 +98,20 @@ After running the command, confirm it succeeded (check the command output for er
|
|
|
98
98
|
If you encounter merge conflicts (e.g., the PR shows conflicts):
|
|
99
99
|
1. Note the conflict in your review comment. Do NOT attempt to resolve — flag it for the author.
|
|
100
100
|
|
|
101
|
+
## Follow-up Dispatch
|
|
102
|
+
|
|
103
|
+
If, while reviewing, you spot substantial work that should be tracked separately rather than blocking this PR (a related-but-distinct bug, a feature gap that didn't ship in this diff, a refactor that's worth doing later), you may dispatch a follow-up Minions work item instead of pinning it as a blocking finding. Read the full contract — decision tree, exact curl shape, required `meta.pr_followup` fields, dedup behavior, and the mandatory comment-thread reply — at:
|
|
104
|
+
|
|
105
|
+
`{{team_root}}/playbooks/templates/followup-dispatch.md`
|
|
106
|
+
|
|
107
|
+
Key rules (full detail in the template):
|
|
108
|
+
|
|
109
|
+
- Use this for **legitimate out-of-scope follow-ups**, not for findings that should block the PR.
|
|
110
|
+
- The review verdict (`APPROVE` / `REQUEST_CHANGES`) is unchanged by the follow-up — pick the verdict based on the PR's own contents.
|
|
111
|
+
- Reply on the originating thread with the new WI id and resolve that sub-thread (do not leave it open).
|
|
112
|
+
- Always include `meta.pr_followup.parent_comment_id` so the server can dedup if you (or another agent) retries.
|
|
113
|
+
- Record every follow-up you dispatched in the `followups` array of your completion JSON.
|
|
114
|
+
|
|
101
115
|
## Do not run git checkout on the main working tree. Use `git diff` and `git show` only.
|
|
102
116
|
|
|
103
117
|
## When to Stop
|
|
@@ -92,6 +92,27 @@ The engine provides a completion report path in the prompt and in `MINIONS_COMPL
|
|
|
92
92
|
|
|
93
93
|
For the canonical schema — every field, the `failure_class` enum, `noop:true` semantics, `retryable` / `needs_rerun` shape, and the artifacts array — see `docs/completion-reports.md`. The JSON report is the primary signal; fenced `completion` blocks in stdout are accepted only as a fallback.
|
|
94
94
|
|
|
95
|
+
## Minions API access
|
|
96
|
+
|
|
97
|
+
The Minions dashboard runs at `http://localhost:7331` whenever the engine is up. Agents may call its HTTP endpoints when (and only when) the playbook explicitly authorizes it for a particular task — most dispatches do not need any API access, and uninvited writes to engine-managed state are still prohibited (see "Do NOT write to `agents/*/status.json`" above).
|
|
98
|
+
|
|
99
|
+
When a playbook authorizes an API call (e.g. follow-up work-item dispatch from `playbooks/templates/followup-dispatch.md`), use the standard `curl` shape:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -sS -X POST http://localhost:7331/api/<route> \
|
|
103
|
+
-H 'Content-Type: application/json' \
|
|
104
|
+
-H 'X-Minions-Agent: <your-agent-id>' \
|
|
105
|
+
-H 'X-Minions-Origin-Wi: <current-work-item-id>' \
|
|
106
|
+
-d '<json-payload>'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Conventions:
|
|
110
|
+
|
|
111
|
+
- The full live route surface is available at `GET http://localhost:7331/api/routes` (the route registry includes method, path, and a one-line description for every handler).
|
|
112
|
+
- **Identification headers (for traceability, not auth).** Set `X-Minions-Agent` to your agent id and `X-Minions-Origin-Wi` to the work-item id you were dispatched against. Endpoints that persist these (e.g. `POST /api/work-items`) store them as `_originAgent` / `_originWi` for the dashboard timeline. Agents do **not** have a `X-CC-Turn-Id` — that header is reserved for Command Center turns.
|
|
113
|
+
- The dashboard binds to `127.0.0.1` only; no auth token is required for localhost calls.
|
|
114
|
+
- Treat HTTP 4xx as a contract violation: surface the response body in your completion summary instead of retrying blindly.
|
|
115
|
+
|
|
95
116
|
## Long-Running Commands
|
|
96
117
|
|
|
97
118
|
Builds, dependency installs, tests, and local servers can be quiet for long periods. Run the repo's normal CLI commands and let them finish; do not add artificial progress output, heartbeat loops, or command-specific workarounds just to keep Minions active.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Follow-up Dispatch (PR-comment → new WI)
|
|
2
|
+
|
|
3
|
+
When a human leaves a PR comment that asks for work that is **legitimate but
|
|
4
|
+
outside the scope of the current PR** (a different bug, a related-but-separate
|
|
5
|
+
feature, a follow-up cleanup that doesn't belong in the current diff), do **not**
|
|
6
|
+
broaden the PR and do **not** silently rebut the comment. Spin off a new Minions
|
|
7
|
+
work item via the dashboard API and reply on the originating comment thread with
|
|
8
|
+
a link to it.
|
|
9
|
+
|
|
10
|
+
This carve-out only enables creating a **separate** work item. It does not
|
|
11
|
+
loosen the rule that the *current* PR should stay focused on the original
|
|
12
|
+
review feedback.
|
|
13
|
+
|
|
14
|
+
## Decision tree
|
|
15
|
+
|
|
16
|
+
For each human comment that asks for additional work, classify it before
|
|
17
|
+
editing:
|
|
18
|
+
|
|
19
|
+
1. **Fix in this PR** — the request is in scope (regression of the current
|
|
20
|
+
diff, a flaw in the new code, missing requested behavior the PR was already
|
|
21
|
+
supposed to cover). Fix it on this branch, push, reply on the thread.
|
|
22
|
+
2. **Answer with rationale** — the request is invalid, stale, already
|
|
23
|
+
addressed, or would harm the code. Post an evidence-backed rebuttal on the
|
|
24
|
+
thread; do **not** create a follow-up WI for it.
|
|
25
|
+
3. **Follow-up WI** — the request is legitimate but clearly out of scope for
|
|
26
|
+
the current PR (different file/system, different bug, distinct feature, a
|
|
27
|
+
refactor the reviewer explicitly asked you to "track separately" or "do in a
|
|
28
|
+
follow-up"). Create a new work item via the API (see below) and reply on the
|
|
29
|
+
originating thread with the new WI id.
|
|
30
|
+
|
|
31
|
+
If you are unsure between #1 and #3, prefer **#3 (follow-up)** so the current
|
|
32
|
+
PR stays small and reviewable. If you are unsure between #2 and #3, prefer #2
|
|
33
|
+
and explain your reasoning — don't manufacture work.
|
|
34
|
+
|
|
35
|
+
## The API call
|
|
36
|
+
|
|
37
|
+
The Minions dashboard runs on `http://localhost:7331`. Create a follow-up work
|
|
38
|
+
item with `POST /api/work-items`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
curl -sS -X POST http://localhost:7331/api/work-items \
|
|
42
|
+
-H 'Content-Type: application/json' \
|
|
43
|
+
-H 'X-Minions-Agent: <your-agent-id>' \
|
|
44
|
+
-H 'X-Minions-Origin-Wi: <current-wi-id>' \
|
|
45
|
+
-d '{
|
|
46
|
+
"title": "Short imperative summary of the follow-up",
|
|
47
|
+
"type": "implement",
|
|
48
|
+
"priority": "medium",
|
|
49
|
+
"project": "<project-name>",
|
|
50
|
+
"description": "Why this is a follow-up, what should change, acceptance.\n\nOriginated from <reviewer> on PR #<N> (<url>): \"<verbatim quote of the ask>\"",
|
|
51
|
+
"references": [
|
|
52
|
+
{"url": "<originating-pr-url>", "label": "Originated from PR #<N> comment"}
|
|
53
|
+
],
|
|
54
|
+
"meta": {
|
|
55
|
+
"pr_followup": {
|
|
56
|
+
"parent_pr_url": "<originating-pr-url>",
|
|
57
|
+
"parent_pr_id": "<canonical PR id, e.g. github:owner/repo#123 or PR-123>",
|
|
58
|
+
"parent_comment_id": "<exact comment/thread id from the host API>",
|
|
59
|
+
"parent_comment_author": "<reviewer login>"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Required fields and their conventions:
|
|
66
|
+
|
|
67
|
+
- **`title`** — short imperative ("Add foo to bar", "Fix off-by-one in baz").
|
|
68
|
+
- **`type`** — pick from the routing types (`implement`, `fix`, `test`,
|
|
69
|
+
`docs`, `ask`, `explore`, …). For new feature/fix work, `implement` is
|
|
70
|
+
almost always correct; use `fix` only when the follow-up is about an existing
|
|
71
|
+
PR's bug. Do **not** set `agent` — let `routing.md` pick.
|
|
72
|
+
- **`project`** — name of the same project the originating PR belongs to.
|
|
73
|
+
Required when more than one project is configured.
|
|
74
|
+
- **`description`** — explain *why* this is a follow-up and quote the exact
|
|
75
|
+
ask. Future readers should understand the scope without re-reading the PR
|
|
76
|
+
thread.
|
|
77
|
+
- **`references`** — at least one entry pointing back at the originating PR.
|
|
78
|
+
- **`meta.pr_followup`** — the four-field shape above. Used by the dashboard
|
|
79
|
+
for traceability chips and by the server for dedup (a second request with the
|
|
80
|
+
same `parent_comment_id` returns the existing WI id rather than creating a
|
|
81
|
+
duplicate).
|
|
82
|
+
|
|
83
|
+
Headers:
|
|
84
|
+
|
|
85
|
+
- **`X-Minions-Agent`** — your agent id (e.g. `lambert`, `ripley`,
|
|
86
|
+
`temp-mp3dop1v…`). Persisted as `_originAgent` on the new WI.
|
|
87
|
+
- **`X-Minions-Origin-Wi`** — the id of the work item you were dispatched
|
|
88
|
+
against (the one whose PR you are responding to). Persisted as `_originWi`.
|
|
89
|
+
|
|
90
|
+
Both headers are optional but strongly recommended — they let the dashboard
|
|
91
|
+
correlate the follow-up to the dispatch that created it.
|
|
92
|
+
|
|
93
|
+
## Server responses
|
|
94
|
+
|
|
95
|
+
| Status | Body | Meaning |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| 200 | `{ ok: true, id: "W-…" }` | New follow-up WI created. |
|
|
98
|
+
| 200 | `{ ok: true, id: "W-…", duplicate: true, duplicateOf: "W-…" }` | The standard title/type/project dedup window matched an in-flight WI. Same WI id is returned; reply on the comment thread with that id. |
|
|
99
|
+
| 409 | `{ error: "followup_already_dispatched", existingWiId: "W-…" }` | Another agent (or your previous attempt) already dispatched a follow-up for this exact `parent_comment_id`. Reply on the comment thread linking the existing WI; do not retry. |
|
|
100
|
+
| 400 | `{ error: "<message>" }` | Validation failed. Inspect the message: the most common cause is a malformed `meta.pr_followup` shape. Fix and retry. |
|
|
101
|
+
|
|
102
|
+
Treat 409 as a **success** for your purposes — the follow-up exists, you just
|
|
103
|
+
weren't the one who created it. Still post the reply on the comment thread with
|
|
104
|
+
the returned `existingWiId`.
|
|
105
|
+
|
|
106
|
+
## Mandatory post-dispatch step
|
|
107
|
+
|
|
108
|
+
After the API call returns a WI id (whether `id` from 200 or `existingWiId`
|
|
109
|
+
from 409), reply on the originating comment thread with a link back:
|
|
110
|
+
|
|
111
|
+
> Tracked as follow-up: **W-…** — see the Minions dashboard. Continuing with
|
|
112
|
+
> the in-place fix on this PR.
|
|
113
|
+
|
|
114
|
+
Then **resolve that sub-thread** (GitHub: "Resolve conversation"; ADO: set
|
|
115
|
+
status to `closed`) even if other review threads on the PR are still open. The
|
|
116
|
+
follow-up WI is the durable artifact; the comment thread should not be left
|
|
117
|
+
hanging.
|
|
118
|
+
|
|
119
|
+
## Record the follow-up in your completion report
|
|
120
|
+
|
|
121
|
+
Add a `followups` array to your JSON completion report so the engine has an
|
|
122
|
+
auditable record of what you fanned out to:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"status": "success",
|
|
127
|
+
"summary": "…",
|
|
128
|
+
"followups": [
|
|
129
|
+
{
|
|
130
|
+
"wi_id": "W-mp7abc…",
|
|
131
|
+
"title": "Add foo to bar",
|
|
132
|
+
"reason": "Out-of-scope ask from reviewer on PR #123 comment #4567890",
|
|
133
|
+
"parent_comment_id": "4567890"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The engine logs each entry at `info` and warns if the array references a WI id
|
|
140
|
+
that doesn't exist in any project's `work-items.json` (mismatch between what
|
|
141
|
+
you claim and what the API actually created — usually a sign that the dispatch
|
|
142
|
+
was reverted or the response was misread).
|
|
143
|
+
|
|
144
|
+
## Do NOT
|
|
145
|
+
|
|
146
|
+
- Do not pick `agent` for the follow-up; the routing layer assigns one based
|
|
147
|
+
on `type` + `project`.
|
|
148
|
+
- Do not set `X-CC-Turn-Id` (that header is for Command Center turns; agents
|
|
149
|
+
don't have a turn id). Use `X-Minions-Agent`/`X-Minions-Origin-Wi` instead.
|
|
150
|
+
- Do not skip the comment-thread reply — without it, the human has no signal
|
|
151
|
+
that their ask was tracked and the engine will keep re-routing the comment
|
|
152
|
+
as `pendingFix`.
|
|
153
|
+
- Do not spin off follow-ups for nits, style notes, or things you are about
|
|
154
|
+
to fix in this PR anyway. Reserve this for asks that genuinely belong in a
|
|
155
|
+
separate WI.
|
|
156
|
+
- Do not omit `meta.pr_followup.parent_comment_id`; without it the server
|
|
157
|
+
cannot dedup and you will create duplicates on every retry.
|