@yemi33/minions 0.1.2392 → 0.1.2394
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 +26 -1
- package/dashboard/styles.css +25 -0
- package/docs/pr-auto-fix-dispatch.md +1 -1
- package/engine/lifecycle.js +50 -20
- package/engine/shared.js +9 -4
- package/engine/work-items-store.js +10 -3
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
package/dashboard/js/qa.js
CHANGED
|
@@ -554,6 +554,8 @@ const QA_SESSION_TERMINAL_STATES = new Set(['done', 'failed', 'killed']);
|
|
|
554
554
|
let _qaSessionsPollInterval = null;
|
|
555
555
|
let _qaSessionsCache = [];
|
|
556
556
|
let _qaRunnersCache = [];
|
|
557
|
+
// Polling replaces the cards every three seconds, so disclosure state lives outside the DOM.
|
|
558
|
+
const _qaExpandedFlows = new Set();
|
|
557
559
|
|
|
558
560
|
function _stopQaSessionsPoll() {
|
|
559
561
|
if (_qaSessionsPollInterval) {
|
|
@@ -706,6 +708,17 @@ function _qaAfterSessionsRender() {
|
|
|
706
708
|
if (allTerminal) _stopQaSessionsPoll();
|
|
707
709
|
}
|
|
708
710
|
|
|
711
|
+
function qaSessionFlowsToggle(details) {
|
|
712
|
+
if (!details) return;
|
|
713
|
+
const isOpen = details.open;
|
|
714
|
+
const summary = details.querySelector('.qa-session-flows-summary');
|
|
715
|
+
if (summary) summary.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
716
|
+
const id = details.getAttribute('data-session-id');
|
|
717
|
+
if (!id) return;
|
|
718
|
+
if (isOpen) _qaExpandedFlows.add(id);
|
|
719
|
+
else _qaExpandedFlows.delete(id);
|
|
720
|
+
}
|
|
721
|
+
|
|
709
722
|
function _qaRenderSessionCard(s) {
|
|
710
723
|
const id = s && s.id || '';
|
|
711
724
|
const state = (s && s.state) || 'unknown';
|
|
@@ -749,7 +762,19 @@ function _qaRenderSessionCard(s) {
|
|
|
749
762
|
|
|
750
763
|
let body = '';
|
|
751
764
|
if (flowsShort) {
|
|
752
|
-
|
|
765
|
+
const flowsOpen = _qaExpandedFlows.has(id);
|
|
766
|
+
body += '<details class="qa-session-flows"' + (flowsOpen ? ' open' : '') +
|
|
767
|
+
' data-session-id="' + escHtml(id) + '" ontoggle="qaSessionFlowsToggle(this)">' +
|
|
768
|
+
'<summary class="qa-session-flows-summary" aria-expanded="' + (flowsOpen ? 'true' : 'false') + '">' +
|
|
769
|
+
'<strong class="qa-session-flows-label">Flows:</strong>' +
|
|
770
|
+
'<span class="qa-session-flows-preview">' + escHtml(flowsShort) + '</span>' +
|
|
771
|
+
'<span class="qa-session-flows-affordance">' +
|
|
772
|
+
'<span class="qa-session-flows-expand-label">Show full prompt</span>' +
|
|
773
|
+
'<span class="qa-session-flows-collapse-label">Hide full prompt</span>' +
|
|
774
|
+
'</span>' +
|
|
775
|
+
'</summary>' +
|
|
776
|
+
'<div class="qa-session-flows-full">' + escHtml(flowsRaw) + '</div>' +
|
|
777
|
+
'</details>';
|
|
753
778
|
}
|
|
754
779
|
if (state === 'failed' && (failureClass || error)) {
|
|
755
780
|
body += '<div class="qa-session-error"><strong>' + escHtml(failureClass || 'failed') + ':</strong> ' + escHtml(error || summary || 'no details') + '</div>';
|
package/dashboard/styles.css
CHANGED
|
@@ -1597,6 +1597,31 @@
|
|
|
1597
1597
|
background: var(--surface); padding: var(--space-3);
|
|
1598
1598
|
border-radius: var(--radius-sm); border: 1px solid var(--border);
|
|
1599
1599
|
}
|
|
1600
|
+
.qa-session-flows-summary {
|
|
1601
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1602
|
+
align-items: start; gap: var(--space-2); cursor: pointer; list-style: none;
|
|
1603
|
+
}
|
|
1604
|
+
.qa-session-flows-summary::-webkit-details-marker { display: none; }
|
|
1605
|
+
.qa-session-flows-summary::marker { content: ''; }
|
|
1606
|
+
.qa-session-flows-summary:focus-visible {
|
|
1607
|
+
outline: 2px solid var(--blue); outline-offset: 3px;
|
|
1608
|
+
border-radius: var(--radius-sm);
|
|
1609
|
+
}
|
|
1610
|
+
.qa-session-flows-preview,
|
|
1611
|
+
.qa-session-flows-full { overflow-wrap: anywhere; }
|
|
1612
|
+
.qa-session-flows-affordance {
|
|
1613
|
+
color: var(--blue); font-size: var(--text-xs); white-space: nowrap;
|
|
1614
|
+
}
|
|
1615
|
+
.qa-session-flows-affordance::before { content: '+ '; }
|
|
1616
|
+
.qa-session-flows-collapse-label { display: none; }
|
|
1617
|
+
.qa-session-flows[open] .qa-session-flows-preview { display: none; }
|
|
1618
|
+
.qa-session-flows[open] .qa-session-flows-affordance::before { content: '- '; }
|
|
1619
|
+
.qa-session-flows[open] .qa-session-flows-expand-label { display: none; }
|
|
1620
|
+
.qa-session-flows[open] .qa-session-flows-collapse-label { display: inline; }
|
|
1621
|
+
.qa-session-flows-full {
|
|
1622
|
+
margin-top: var(--space-3); padding-top: var(--space-3);
|
|
1623
|
+
border-top: 1px solid var(--border); white-space: pre-wrap;
|
|
1624
|
+
}
|
|
1600
1625
|
.qa-session-error {
|
|
1601
1626
|
font-size: var(--text-base); color: var(--red);
|
|
1602
1627
|
background: rgba(248, 81, 73, 0.08); padding: var(--space-3);
|
|
@@ -51,7 +51,7 @@ Independent of the master kill-switches, each PR tracks repeated no-op fix outco
|
|
|
51
51
|
2. Click the red chip → `POST /api/pull-requests/clear-paused-cause` with `{ prId, cause }`.
|
|
52
52
|
3. Direct API call to the same endpoint.
|
|
53
53
|
|
|
54
|
-
This per-PR per-cause pause is unrelated to `autoFixPaused`. Cause
|
|
54
|
+
This per-PR per-cause pause is unrelated to `autoFixPaused`. Cause values are validated against `Object.values(shared.PR_FIX_CAUSE)` (`human-feedback`, `review-feedback`, `build-failure`, `merge-conflict`, or `pr-fix`). Full mechanics in [`docs/pr-review-fix-loop.md`](pr-review-fix-loop.md) §4E.
|
|
55
55
|
|
|
56
56
|
## Key files
|
|
57
57
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -2629,7 +2629,7 @@ function normalizePrFixBranchName(branch) {
|
|
|
2629
2629
|
}
|
|
2630
2630
|
|
|
2631
2631
|
function getPrFixBaselineHead(pr) {
|
|
2632
|
-
return String(pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
|
|
2632
|
+
return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
|
|
2633
2633
|
}
|
|
2634
2634
|
|
|
2635
2635
|
function findPrFixWorktree(meta, project, config) {
|
|
@@ -5962,7 +5962,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5962
5962
|
}
|
|
5963
5963
|
if (effectiveSuccess && !skipDoneStatus) {
|
|
5964
5964
|
// Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
|
|
5965
|
-
|
|
5965
|
+
const verifiedHeadSha = prFixBranchChange?.evidence === 'remote-head'
|
|
5966
|
+
? prFixBranchChange.afterHead
|
|
5967
|
+
: '';
|
|
5968
|
+
try {
|
|
5969
|
+
autoDispatchLiveValidationWi(meta, config, dispatchItem, { verifiedHeadSha });
|
|
5970
|
+
} catch (err) {
|
|
5971
|
+
log('warn', `autoDispatchLiveValidationWi: ${err.message}`);
|
|
5972
|
+
}
|
|
5966
5973
|
}
|
|
5967
5974
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
5968
5975
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
@@ -6943,9 +6950,9 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6943
6950
|
// always a canonical `test` WI with the PR-focused `build-and-test` playbook;
|
|
6944
6951
|
// liveValidation.type controls routing, not the semantic type of the task.
|
|
6945
6952
|
//
|
|
6946
|
-
// Deduplicates per codingWiId
|
|
6947
|
-
//
|
|
6948
|
-
//
|
|
6953
|
+
// Deduplicates both per codingWiId and per canonical PR source revision. This
|
|
6954
|
+
// keeps concurrent/repeated fixes for one unchanged PR head from queueing
|
|
6955
|
+
// duplicate validations while allowing a fresh validation after the head moves.
|
|
6949
6956
|
//
|
|
6950
6957
|
// W-mr9u39az000db5c2 — guard against a self-referential recursive cascade.
|
|
6951
6958
|
// When liveValidation.type overlaps with a coding WI's own type set (e.g.
|
|
@@ -6970,7 +6977,7 @@ const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
|
|
|
6970
6977
|
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
6971
6978
|
]);
|
|
6972
6979
|
|
|
6973
|
-
function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
6980
|
+
function autoDispatchLiveValidationWi(meta, config, dispatchItem = null, options = {}) {
|
|
6974
6981
|
const item = meta?.item || null;
|
|
6975
6982
|
const sourceType = item?.type || dispatchItem?.type || null;
|
|
6976
6983
|
const sourceKey = item?.id || dispatchItem?.id || null;
|
|
@@ -7033,27 +7040,48 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
|
7033
7040
|
const prTarget = prUrl || (prId != null ? String(prId) : null);
|
|
7034
7041
|
if (!prTarget) return;
|
|
7035
7042
|
|
|
7043
|
+
const prIdentityRef = {
|
|
7044
|
+
id: prId != null ? String(prId) : prTarget,
|
|
7045
|
+
url: prUrl || '',
|
|
7046
|
+
};
|
|
7047
|
+
let currentPr = typeof prRef === 'object' ? prRef : null;
|
|
7048
|
+
try {
|
|
7049
|
+
currentPr = shared.findPrRecord(shared.readPullRequests(project), prIdentityRef, project) || currentPr;
|
|
7050
|
+
} catch (err) {
|
|
7051
|
+
log('warn', `liveValidation: could not resolve tracked PR ${prTarget}: ${err.message}`);
|
|
7052
|
+
}
|
|
7053
|
+
const liveValidationPrId = shared.getPrIdentityKey(currentPr || prIdentityRef, project);
|
|
7054
|
+
if (!liveValidationPrId) return;
|
|
7055
|
+
const liveValidationHeadSha = String(options?.verifiedHeadSha || '').trim()
|
|
7056
|
+
|| getPrFixBaselineHead(currentPr);
|
|
7057
|
+
|
|
7036
7058
|
const codingWiId = sourceKey;
|
|
7037
7059
|
const wiScope = item ? resolveWorkItemScope(meta) : projectName;
|
|
7038
7060
|
if (!wiScope) return;
|
|
7039
7061
|
|
|
7040
7062
|
try {
|
|
7041
|
-
mutateWorkItems(wiScope, items => {
|
|
7063
|
+
mutateWorkItems(wiScope, (items, comparisonItems) => {
|
|
7042
7064
|
if (!Array.isArray(items)) return items;
|
|
7043
7065
|
|
|
7044
|
-
//
|
|
7045
|
-
//
|
|
7046
|
-
//
|
|
7047
|
-
//
|
|
7048
|
-
//
|
|
7049
|
-
const existing =
|
|
7050
|
-
i
|
|
7051
|
-
i.meta
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7066
|
+
// Keep the source-WI guard and revision guard in this same mutation as the
|
|
7067
|
+
// insert, and compare both supported scopes while the SQLite write
|
|
7068
|
+
// transaction is held. Legacy live-validation WIs predate the persisted
|
|
7069
|
+
// revision fields, so derive their identity from structured PR fields and
|
|
7070
|
+
// conservatively treat a missing head as matching until terminal.
|
|
7071
|
+
const existing = comparisonItems.find(i => {
|
|
7072
|
+
if (!i?.meta?.liveValidationFor || PLAN_TERMINAL_STATUSES.has(i.status)) return false;
|
|
7073
|
+
if (i.meta.liveValidationFor === codingWiId) return true;
|
|
7074
|
+
|
|
7075
|
+
const existingPrRef = i.meta.liveValidationPrId || shared.extractStructuredWorkItemPrRef(i);
|
|
7076
|
+
if (!existingPrRef) return false;
|
|
7077
|
+
const existingPrId = shared.getPrIdentityKey(existingPrRef, project);
|
|
7078
|
+
if (existingPrId !== liveValidationPrId) return false;
|
|
7079
|
+
|
|
7080
|
+
const existingHeadSha = String(i.meta.liveValidationHeadSha || '').trim();
|
|
7081
|
+
return !existingHeadSha || !liveValidationHeadSha || existingHeadSha === liveValidationHeadSha;
|
|
7082
|
+
});
|
|
7055
7083
|
if (existing) {
|
|
7056
|
-
log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id}
|
|
7084
|
+
log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} blocks another validation for ${codingWiId} (${liveValidationPrId}@${liveValidationHeadSha || 'unknown'})`);
|
|
7057
7085
|
return items;
|
|
7058
7086
|
}
|
|
7059
7087
|
|
|
@@ -7081,6 +7109,8 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
|
7081
7109
|
meta: {
|
|
7082
7110
|
liveValidationFor: codingWiId,
|
|
7083
7111
|
liveValidationType: validationType,
|
|
7112
|
+
liveValidationPrId,
|
|
7113
|
+
...(liveValidationHeadSha ? { liveValidationHeadSha } : {}),
|
|
7084
7114
|
playbook: 'build-and-test',
|
|
7085
7115
|
...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
|
|
7086
7116
|
},
|
|
@@ -7093,7 +7123,7 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
|
7093
7123
|
items.push(validationWi);
|
|
7094
7124
|
log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
|
|
7095
7125
|
return items;
|
|
7096
|
-
});
|
|
7126
|
+
}, { readScopes: ['central', projectName] });
|
|
7097
7127
|
} catch (err) {
|
|
7098
7128
|
log('warn', `autoDispatchLiveValidationWi for ${codingWiId}: ${err.message}`);
|
|
7099
7129
|
}
|
package/engine/shared.js
CHANGED
|
@@ -7702,12 +7702,17 @@ function readWorkItems(source = null) {
|
|
|
7702
7702
|
return require('./work-items-store').readWorkItemsForScope(stateScope(source));
|
|
7703
7703
|
}
|
|
7704
7704
|
|
|
7705
|
-
function mutateWorkItems(source, mutator) {
|
|
7705
|
+
function mutateWorkItems(source, mutator, opts = {}) {
|
|
7706
7706
|
const store = require('./work-items-store');
|
|
7707
|
-
const
|
|
7707
|
+
const scope = stateScope(source);
|
|
7708
|
+
const normalizedOpts = { ...opts };
|
|
7709
|
+
if (Array.isArray(opts.readScopes)) {
|
|
7710
|
+
normalizedOpts.readScopes = opts.readScopes.map(stateScope);
|
|
7711
|
+
}
|
|
7712
|
+
const { wrote, result } = store.applyWorkItemsMutation(scope, (items, comparisonItems) => {
|
|
7708
7713
|
if (!Array.isArray(items)) items = [];
|
|
7709
|
-
return mutator(items) || items;
|
|
7710
|
-
});
|
|
7714
|
+
return mutator(items, comparisonItems) || items;
|
|
7715
|
+
}, normalizedOpts);
|
|
7711
7716
|
if (wrote) {
|
|
7712
7717
|
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
7713
7718
|
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
@@ -128,13 +128,14 @@ function _applyWorkItemsDiff(db, diff) {
|
|
|
128
128
|
|
|
129
129
|
// Apply a mutation to the work-items array for a given scope. Wrapped in
|
|
130
130
|
// a single transaction so the diff and apply can't race against a
|
|
131
|
-
// concurrent reader/writer.
|
|
131
|
+
// concurrent reader/writer. Optional readScopes are exposed to the mutator
|
|
132
|
+
// as a combined comparison set; only target-scope mutations are persisted.
|
|
132
133
|
//
|
|
133
134
|
// Returns { wrote, result }:
|
|
134
135
|
// wrote — true iff at least one INSERT/UPDATE/DELETE landed
|
|
135
136
|
// result — the post-mutation array (legacy return shape of
|
|
136
137
|
// mutateJsonFileLocked)
|
|
137
|
-
function applyWorkItemsMutation(scope, mutator, opts) {
|
|
138
|
+
function applyWorkItemsMutation(scope, mutator, opts = {}) {
|
|
138
139
|
const { getDb, withTransaction } = require('./db');
|
|
139
140
|
const db = getDb();
|
|
140
141
|
|
|
@@ -144,8 +145,14 @@ function applyWorkItemsMutation(scope, mutator, opts) {
|
|
|
144
145
|
// added by readAllWorkItems only and would round-trip as garbage into
|
|
145
146
|
// the data column otherwise.
|
|
146
147
|
for (const wi of before) { if (wi && wi._source) delete wi._source; }
|
|
148
|
+
const comparisonItems = [...before];
|
|
149
|
+
const readScopes = Array.isArray(opts.readScopes) ? opts.readScopes : [];
|
|
150
|
+
for (const readScope of new Set(readScopes)) {
|
|
151
|
+
if (readScope === scope) continue;
|
|
152
|
+
comparisonItems.push(...readWorkItemsForScope(readScope));
|
|
153
|
+
}
|
|
147
154
|
const beforeSnapshot = JSON.parse(JSON.stringify(before));
|
|
148
|
-
const next = mutator(before);
|
|
155
|
+
const next = mutator(before, comparisonItems);
|
|
149
156
|
const after = (next === undefined || next === null)
|
|
150
157
|
? before
|
|
151
158
|
: (Array.isArray(next) ? next : before);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2394",
|
|
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/plan-to-prd.md
CHANGED
|
@@ -135,7 +135,7 @@ Rules for items:
|
|
|
135
135
|
{ "id": "P-bbb2", "name": "UI client", "project": "web-dashboard", ... }
|
|
136
136
|
]
|
|
137
137
|
```
|
|
138
|
-
The engine routes each item to that project's SQL work-item scope.
|
|
138
|
+
The engine routes each item to that project's SQL work-item scope. At plan completion, the engine creates one project-scoped verify work item per touched project (each project with an active PR linked to a completed plan item). If no linked PRs surface, it falls back to one verify work item on the primary project. See the **Cross-repo plans** section below for the full output contract when `{{target_projects}}` is set.
|
|
139
139
|
- `depends_on` lists IDs of items that must be done first
|
|
140
140
|
- Keep descriptions actionable — name the files, functions, patterns, or integration points the implementing agent should touch whenever the plan makes them clear
|
|
141
141
|
- Include `acceptance_criteria` so reviewers know when it's done
|
|
@@ -161,7 +161,7 @@ Apply these rules on top of (and where they conflict, instead of) the single-pro
|
|
|
161
161
|
- `parallel`: omit `feature_branch` entirely (the engine derives per-item branches as `user/<loginname>/<wi-id>-<slug>` in each project).
|
|
162
162
|
- `shared-branch` (only with explicit justification): use the canonical `user/<loginname>/PL-<short-kebab-slug>` form. The engine will pre-create a matching branch in every project listed in `{{target_projects}}` (P-1c0f5e84) so each item can push to the same branch name in its own repo.
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
At plan completion, the engine creates one verify work item per touched project, where a touched project has at least one active PR linked to a completed plan item. Those project-scoped verify tasks run independently in parallel. If no linked PRs surface (for example, items completed without PR records or legacy state), the fan-out collapses to one verify work item on the primary project — the project with the most completed items — as a fallback.
|
|
165
165
|
|
|
166
166
|
{{/target_projects}}
|
|
167
167
|
## Reusing an Existing PRD
|