@yemi33/minions 0.1.1998 → 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/qa.js +509 -24
- package/dashboard/js/render-prs.js +23 -1
- package/dashboard/js/render-work-items.js +11 -1
- package/dashboard/pages/qa.html +53 -6
- package/dashboard/styles.css +149 -0
- 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
package/dashboard/pages/qa.html
CHANGED
|
@@ -1,11 +1,58 @@
|
|
|
1
1
|
<section>
|
|
2
2
|
<h2>QA</h2>
|
|
3
|
-
<p class="empty" style="margin:4px 0 12px 0">
|
|
3
|
+
<p class="empty" style="margin:4px 0 12px 0">Validation runbooks dispatched against live managed instances. Targets, runbooks, and run history with artifact previews.</p>
|
|
4
4
|
</section>
|
|
5
|
-
<section id="qa-
|
|
6
|
-
<h2>
|
|
7
|
-
<div
|
|
8
|
-
<p class="empty"
|
|
9
|
-
|
|
5
|
+
<section id="qa-targets-section" class="qa-section">
|
|
6
|
+
<h2>Targets <span class="qa-section-subtitle">live managed/keep processes available as runbook targets — full inventory lives on <a href="/engine" class="qa-engine-link">/engine</a></span></h2>
|
|
7
|
+
<div id="qa-targets-content" class="qa-targets-list">
|
|
8
|
+
<p class="empty">Loading targets…</p>
|
|
9
|
+
</div>
|
|
10
|
+
</section>
|
|
11
|
+
<section id="qa-runbooks-section" class="qa-section">
|
|
12
|
+
<h2>Validation Runbooks <span class="qa-section-subtitle">human or agent-driven smoke / E2E flows against the targets above</span></h2>
|
|
13
|
+
<div class="qa-runbooks-actions">
|
|
14
|
+
<button id="qa-new-runbook-btn" class="qa-btn-primary" onclick="qaShowNewRunbookForm()">+ New runbook</button>
|
|
15
|
+
</div>
|
|
16
|
+
<div id="qa-runbook-form-wrap" class="qa-runbook-form" style="display:none">
|
|
17
|
+
<form id="qa-runbook-form" onsubmit="event.preventDefault();qaSaveRunbook();">
|
|
18
|
+
<div class="qa-form-row">
|
|
19
|
+
<label class="qa-form-label" for="qa-runbook-name">Name</label>
|
|
20
|
+
<input id="qa-runbook-name" type="text" class="qa-form-input" placeholder="login-smoke" required>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="qa-form-row">
|
|
23
|
+
<label class="qa-form-label" for="qa-runbook-target">Target</label>
|
|
24
|
+
<select id="qa-runbook-target" class="qa-form-input" required>
|
|
25
|
+
<option value="">— select a target —</option>
|
|
26
|
+
</select>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="qa-form-row">
|
|
29
|
+
<label class="qa-form-label" for="qa-runbook-steps">Steps</label>
|
|
30
|
+
<textarea id="qa-runbook-steps" class="qa-form-input qa-form-textarea" placeholder="1. Open the app 2. Click login 3. Verify dashboard loads" required></textarea>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="qa-form-row">
|
|
33
|
+
<label class="qa-form-label">Expected artifacts</label>
|
|
34
|
+
<div id="qa-runbook-artifacts" class="qa-artifacts-repeater">
|
|
35
|
+
<div class="qa-artifact-row">
|
|
36
|
+
<input type="text" class="qa-form-input qa-artifact-input" placeholder="screenshot:dashboard.png">
|
|
37
|
+
<button type="button" class="qa-btn-ghost" onclick="qaRemoveArtifactRow(this)">−</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<button type="button" class="qa-btn-ghost qa-add-artifact-btn" onclick="qaAddArtifactRow()">+ Add artifact</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="qa-form-row qa-form-actions">
|
|
43
|
+
<button type="submit" class="qa-btn-primary">Save</button>
|
|
44
|
+
<button type="button" class="qa-btn-ghost" onclick="qaHideNewRunbookForm()">Cancel</button>
|
|
45
|
+
<span id="qa-runbook-form-msg" class="qa-form-msg"></span>
|
|
46
|
+
</div>
|
|
47
|
+
</form>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="qa-runbooks-content" class="qa-runbooks-list">
|
|
50
|
+
<p class="empty">Loading runbooks…</p>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
<section id="qa-runs-section" class="qa-section">
|
|
54
|
+
<h2>Recent Runs <span class="qa-section-subtitle">latest 50 dispatched validation runs — polled every 5s while this page is active</span></h2>
|
|
55
|
+
<div id="qa-runs-content" class="qa-runs-list">
|
|
56
|
+
<p class="empty">Loading runs…</p>
|
|
10
57
|
</div>
|
|
11
58
|
</section>
|
package/dashboard/styles.css
CHANGED
|
@@ -736,3 +736,152 @@
|
|
|
736
736
|
display: inline-block; max-width: 240px; overflow: hidden;
|
|
737
737
|
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
|
|
738
738
|
}
|
|
739
|
+
|
|
740
|
+
/* QA tab (W-mpeiwz6k0005bf34-d) — targets / runbooks / runs sections.
|
|
741
|
+
* Reuses surface/border/text tokens defined in :root so the QA page
|
|
742
|
+
* blends with the rest of the dashboard. */
|
|
743
|
+
.qa-section { padding: var(--space-7) var(--space-9); }
|
|
744
|
+
.qa-section h2 { display: flex; align-items: baseline; gap: var(--space-4); }
|
|
745
|
+
.qa-section-subtitle {
|
|
746
|
+
font-size: var(--text-sm); color: var(--muted);
|
|
747
|
+
font-weight: 400; text-transform: none; letter-spacing: 0;
|
|
748
|
+
}
|
|
749
|
+
.qa-engine-link { color: var(--blue); text-decoration: none; }
|
|
750
|
+
.qa-engine-link:hover { text-decoration: underline; }
|
|
751
|
+
|
|
752
|
+
/* Targets list — slim row per dedup'd target. */
|
|
753
|
+
.qa-targets-list {
|
|
754
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
755
|
+
border: 1px solid var(--border); border-radius: var(--radius-md);
|
|
756
|
+
background: var(--surface2); padding: var(--space-4);
|
|
757
|
+
}
|
|
758
|
+
.qa-target-row {
|
|
759
|
+
display: flex; align-items: center; gap: var(--space-5);
|
|
760
|
+
padding: var(--space-3) var(--space-4);
|
|
761
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
762
|
+
border-radius: var(--radius-sm); font-size: var(--text-md);
|
|
763
|
+
}
|
|
764
|
+
.qa-target-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
765
|
+
.qa-target-name { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; color: var(--text); }
|
|
766
|
+
.qa-target-project { color: var(--muted); font-size: var(--text-base); }
|
|
767
|
+
.qa-target-source {
|
|
768
|
+
display: inline-block; margin-left: var(--space-3);
|
|
769
|
+
font-size: var(--text-xs); color: var(--muted);
|
|
770
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
771
|
+
padding: 1px var(--space-3); text-transform: uppercase;
|
|
772
|
+
}
|
|
773
|
+
.qa-target-meta { font-size: var(--text-base); color: var(--muted); white-space: nowrap; }
|
|
774
|
+
.qa-target-link {
|
|
775
|
+
font-size: var(--text-sm); color: var(--blue); text-decoration: none;
|
|
776
|
+
padding: var(--space-1) var(--space-4); border: 1px solid var(--border);
|
|
777
|
+
border-radius: var(--radius-sm); background: var(--surface2);
|
|
778
|
+
}
|
|
779
|
+
.qa-target-link:hover { background: var(--bg); }
|
|
780
|
+
.qa-health-dot { font-size: var(--text-md); }
|
|
781
|
+
.qa-health-ok { color: var(--green); }
|
|
782
|
+
.qa-health-warn { color: var(--yellow); }
|
|
783
|
+
.qa-health-down { color: var(--muted); }
|
|
784
|
+
|
|
785
|
+
/* Runbooks list + inline new-runbook form. */
|
|
786
|
+
.qa-runbooks-actions { margin-bottom: var(--space-5); }
|
|
787
|
+
.qa-runbook-form {
|
|
788
|
+
border: 1px solid var(--border); border-radius: var(--radius-md);
|
|
789
|
+
background: var(--surface2); padding: var(--space-5);
|
|
790
|
+
margin-bottom: var(--space-5);
|
|
791
|
+
}
|
|
792
|
+
.qa-form-row { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
|
|
793
|
+
.qa-form-label { font-size: var(--text-base); color: var(--muted); font-weight: 600; }
|
|
794
|
+
.qa-form-input {
|
|
795
|
+
background: var(--bg); color: var(--text);
|
|
796
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
797
|
+
padding: var(--space-3) var(--space-4); font-size: var(--text-md);
|
|
798
|
+
font-family: inherit;
|
|
799
|
+
}
|
|
800
|
+
.qa-form-input:focus { border-color: var(--blue); outline: none; }
|
|
801
|
+
.qa-form-textarea { min-height: 88px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
|
|
802
|
+
.qa-artifacts-repeater { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
803
|
+
.qa-artifact-row { display: flex; gap: var(--space-3); align-items: center; }
|
|
804
|
+
.qa-artifact-input { flex: 1; }
|
|
805
|
+
.qa-add-artifact-btn { align-self: flex-start; margin-top: var(--space-2); }
|
|
806
|
+
.qa-form-actions { flex-direction: row; align-items: center; gap: var(--space-4); }
|
|
807
|
+
.qa-form-msg { font-size: var(--text-base); margin-left: var(--space-3); }
|
|
808
|
+
.qa-btn-primary {
|
|
809
|
+
background: var(--blue); color: #fff; border: none;
|
|
810
|
+
border-radius: var(--radius-sm); padding: var(--space-3) var(--space-7);
|
|
811
|
+
font-size: var(--text-md); font-weight: 600; cursor: pointer;
|
|
812
|
+
}
|
|
813
|
+
.qa-btn-primary:hover { opacity: 0.9; }
|
|
814
|
+
.qa-btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
815
|
+
.qa-btn-ghost {
|
|
816
|
+
background: transparent; color: var(--muted);
|
|
817
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
818
|
+
padding: var(--space-2) var(--space-5); font-size: var(--text-base);
|
|
819
|
+
cursor: pointer;
|
|
820
|
+
}
|
|
821
|
+
.qa-btn-ghost:hover { color: var(--text); background: var(--surface); }
|
|
822
|
+
|
|
823
|
+
.qa-runbooks-list { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
824
|
+
.qa-runbook-row {
|
|
825
|
+
display: flex; align-items: center; gap: var(--space-5);
|
|
826
|
+
padding: var(--space-4) var(--space-5);
|
|
827
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
828
|
+
border-radius: var(--radius-sm);
|
|
829
|
+
}
|
|
830
|
+
.qa-runbook-main { flex: 1; min-width: 0; }
|
|
831
|
+
.qa-runbook-name { font-weight: 600; color: var(--text); font-size: var(--text-md); }
|
|
832
|
+
.qa-runbook-meta { font-size: var(--text-base); color: var(--muted); margin-top: var(--space-1); }
|
|
833
|
+
|
|
834
|
+
/* Runs list — status badge + inline artifact previews. */
|
|
835
|
+
.qa-runs-list { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
836
|
+
.qa-run-row {
|
|
837
|
+
border: 1px solid var(--border); border-radius: var(--radius-md);
|
|
838
|
+
background: var(--surface2); padding: var(--space-4) var(--space-5);
|
|
839
|
+
}
|
|
840
|
+
.qa-run-head { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; font-size: var(--text-md); }
|
|
841
|
+
.qa-run-status {
|
|
842
|
+
display: inline-block; font-size: var(--text-xs); font-weight: 700;
|
|
843
|
+
padding: 2px var(--space-3); border-radius: var(--radius-sm);
|
|
844
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
845
|
+
background: var(--surface); color: var(--muted); border: 1px solid var(--border);
|
|
846
|
+
}
|
|
847
|
+
.qa-status-passed, .qa-status-success { color: var(--green); border-color: var(--green); }
|
|
848
|
+
.qa-status-failed, .qa-status-error { color: var(--red); border-color: var(--red); }
|
|
849
|
+
.qa-status-running, .qa-status-dispatched { color: var(--blue); border-color: var(--blue); }
|
|
850
|
+
.qa-status-pending { color: var(--yellow); border-color: var(--yellow); }
|
|
851
|
+
.qa-run-name { font-weight: 600; color: var(--text); }
|
|
852
|
+
.qa-run-target { color: var(--muted); font-size: var(--text-base); }
|
|
853
|
+
.qa-run-ts { color: var(--muted); font-size: var(--text-base); margin-left: auto; }
|
|
854
|
+
.qa-run-actions { margin-left: var(--space-5); }
|
|
855
|
+
.qa-run-link { color: var(--blue); text-decoration: none; font-size: var(--text-base); }
|
|
856
|
+
.qa-run-link:hover { text-decoration: underline; }
|
|
857
|
+
|
|
858
|
+
.qa-run-artifacts {
|
|
859
|
+
display: flex; flex-wrap: wrap; gap: var(--space-4);
|
|
860
|
+
margin-top: var(--space-4); padding-top: var(--space-4);
|
|
861
|
+
border-top: 1px solid var(--border);
|
|
862
|
+
}
|
|
863
|
+
.qa-artifact {
|
|
864
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
865
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
866
|
+
border-radius: var(--radius-sm); padding: var(--space-3);
|
|
867
|
+
max-width: 320px;
|
|
868
|
+
}
|
|
869
|
+
.qa-artifact img, .qa-artifact video {
|
|
870
|
+
max-width: 100%; max-height: 200px; border-radius: var(--radius-sm);
|
|
871
|
+
background: var(--bg);
|
|
872
|
+
}
|
|
873
|
+
.qa-artifact-caption {
|
|
874
|
+
font-size: var(--text-xs); color: var(--muted);
|
|
875
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
876
|
+
word-break: break-all;
|
|
877
|
+
}
|
|
878
|
+
.qa-artifact-fulllink { color: var(--blue); text-decoration: none; margin-left: var(--space-3); }
|
|
879
|
+
.qa-artifact-fulllink:hover { text-decoration: underline; }
|
|
880
|
+
.qa-artifact-log-preview {
|
|
881
|
+
background: var(--bg); color: var(--text);
|
|
882
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
883
|
+
font-size: var(--text-xs); padding: var(--space-3);
|
|
884
|
+
border-radius: var(--radius-sm); margin: 0;
|
|
885
|
+
max-height: 160px; overflow: auto; white-space: pre;
|
|
886
|
+
}
|
|
887
|
+
.qa-artifact-log { max-width: 480px; }
|
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
|