@yemi33/minions 0.1.2196 → 0.1.2197
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/modal-qa.js +21 -3
- package/engine/dispatch.js +39 -4
- package/engine/lifecycle.js +21 -7
- package/engine/shared.js +35 -13
- package/engine.js +4 -6
- package/package.json +1 -1
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -335,6 +335,19 @@ function _qaBuildRawErrorHtml(err) {
|
|
|
335
335
|
'</details>';
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
function _qaDocStreamErrorMessage(evt) {
|
|
339
|
+
if (!evt) return 'Failed';
|
|
340
|
+
if (typeof evt.error === 'string' && evt.error) return evt.error;
|
|
341
|
+
if (evt.message) return String(evt.message);
|
|
342
|
+
if (evt.errorMessage) return String(evt.errorMessage);
|
|
343
|
+
if (evt.error && typeof evt.error === 'object') {
|
|
344
|
+
if (evt.error.errorMessage) return String(evt.error.errorMessage);
|
|
345
|
+
if (evt.error.message) return String(evt.error.message);
|
|
346
|
+
if (evt.error.stderr) return String(evt.error.stderr);
|
|
347
|
+
}
|
|
348
|
+
return 'Failed';
|
|
349
|
+
}
|
|
350
|
+
|
|
338
351
|
function _qaBuildAssistantHtml(text, opts) {
|
|
339
352
|
const body = opts?.isError ? escHtml(text) : renderMd(text);
|
|
340
353
|
const style = opts?.isError
|
|
@@ -713,6 +726,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
713
726
|
const decoder = new TextDecoder();
|
|
714
727
|
let buf = '';
|
|
715
728
|
let terminalEventSeen = false;
|
|
729
|
+
let pendingStreamErrorEvent = null;
|
|
716
730
|
|
|
717
731
|
async function _qaHandleStreamEvent(evt) {
|
|
718
732
|
if (!evt || !evt.type) return;
|
|
@@ -842,8 +856,8 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
842
856
|
return;
|
|
843
857
|
}
|
|
844
858
|
if (evt.type === 'error') {
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
pendingStreamErrorEvent = evt;
|
|
860
|
+
return;
|
|
847
861
|
}
|
|
848
862
|
}
|
|
849
863
|
|
|
@@ -867,7 +881,11 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
867
881
|
await _qaHandleStreamEvent(JSON.parse(line.slice(6)));
|
|
868
882
|
}
|
|
869
883
|
}
|
|
870
|
-
if (!terminalEventSeen)
|
|
884
|
+
if (!terminalEventSeen) {
|
|
885
|
+
throw new Error(pendingStreamErrorEvent
|
|
886
|
+
? _qaDocStreamErrorMessage(pendingStreamErrorEvent)
|
|
887
|
+
: 'The response stream ended before completion.');
|
|
888
|
+
}
|
|
871
889
|
} catch (e) {
|
|
872
890
|
clearInterval(qaTimer);
|
|
873
891
|
_clearQaStreamWatchdog();
|
package/engine/dispatch.js
CHANGED
|
@@ -12,7 +12,7 @@ const dispatchEvents = require('./dispatch-events');
|
|
|
12
12
|
|
|
13
13
|
const { safeJsonArr, mutateWorkItems,
|
|
14
14
|
mutatePullRequests, getProjects, projectPrPath, log, ts, dateStamp,
|
|
15
|
-
sidecarDispatchPrompt, deleteDispatchPromptSidecar,
|
|
15
|
+
sidecarDispatchPrompt, deleteDispatchPromptSidecar, DONE_STATUSES,
|
|
16
16
|
WI_STATUS, WORK_TYPE, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS, PR_STATUS } = shared;
|
|
17
17
|
const { getConfig, INBOX_DIR } = queries;
|
|
18
18
|
|
|
@@ -426,12 +426,12 @@ function getStalePrDispatchReason(entry, config) {
|
|
|
426
426
|
// 1. `entry.meta?.source !== 'work-item'` — blocks any non-work-item dispatch
|
|
427
427
|
// (e.g. legacy `pr`/`pr-human-feedback` discovery paths) from running
|
|
428
428
|
// against a context-only PR. Auto-discovery in engine.js#discoverFromPrs
|
|
429
|
-
// already skips
|
|
429
|
+
// already skips context-only PRs at the source, so this is defense-in-depth.
|
|
430
430
|
// 2. `!NON_MUTATING_DISPATCH_TYPES.has(entry.type)` — even an explicit
|
|
431
431
|
// work-item dispatch is dropped if it's a mutating type (fix, implement,
|
|
432
432
|
// test, verify, decompose, docs). Only review/ask/explore — which never
|
|
433
433
|
// push to the PR's source branch — are allowed through.
|
|
434
|
-
if (tracked
|
|
434
|
+
if (shared.isContextOnlyPrRecord(tracked)
|
|
435
435
|
&& (entry.meta?.source !== 'work-item' || !NON_MUTATING_DISPATCH_TYPES.has(entry.type))) {
|
|
436
436
|
return `PR ${tracked.id || prLabel} is context-only`;
|
|
437
437
|
}
|
|
@@ -445,6 +445,40 @@ function getStalePrDispatchReason(entry, config) {
|
|
|
445
445
|
return '';
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
function cancelSourceWorkItemForPrunedDispatch(entry, reason) {
|
|
449
|
+
const itemId = entry?.meta?.item?.id;
|
|
450
|
+
if (!itemId) return false;
|
|
451
|
+
let wiPath;
|
|
452
|
+
try { wiPath = lifecycle().resolveWorkItemPath(entry.meta); }
|
|
453
|
+
catch (e) {
|
|
454
|
+
log('warn', `Failed to resolve source work item for pruned dispatch ${entry.id}: ${e.message}`);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
if (!wiPath) return false;
|
|
458
|
+
|
|
459
|
+
let cancelled = false;
|
|
460
|
+
try {
|
|
461
|
+
mutateWorkItems(wiPath, (items) => {
|
|
462
|
+
if (!Array.isArray(items)) return items;
|
|
463
|
+
const wi = items.find(w => w && w.id === itemId);
|
|
464
|
+
if (!wi) return items;
|
|
465
|
+
if (DONE_STATUSES.has(wi.status) || wi.status === WI_STATUS.FAILED || wi.status === WI_STATUS.CANCELLED) return items;
|
|
466
|
+
wi.status = WI_STATUS.CANCELLED;
|
|
467
|
+
wi._cancelledBy = reason || 'stale PR dispatch pruned';
|
|
468
|
+
wi.cancelledAt = ts();
|
|
469
|
+
wi._lastDispatchResult = 'pruned-stale-pr-dispatch';
|
|
470
|
+
delete wi.dispatched_at;
|
|
471
|
+
delete wi.dispatched_to;
|
|
472
|
+
delete wi._pendingReason;
|
|
473
|
+
cancelled = true;
|
|
474
|
+
return items;
|
|
475
|
+
}, { skipWriteIfUnchanged: true });
|
|
476
|
+
} catch (e) {
|
|
477
|
+
log('warn', `Failed to cancel work item ${itemId} for pruned dispatch ${entry.id}: ${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
return cancelled;
|
|
480
|
+
}
|
|
481
|
+
|
|
448
482
|
function pruneStalePrDispatches(config = queries.getConfig()) {
|
|
449
483
|
const removed = [];
|
|
450
484
|
mutateDispatch((dispatch) => {
|
|
@@ -459,7 +493,8 @@ function pruneStalePrDispatches(config = queries.getConfig()) {
|
|
|
459
493
|
|
|
460
494
|
for (const { entry, reason } of removed) {
|
|
461
495
|
try { deleteDispatchPromptSidecar(entry); } catch { /* cleanup best-effort */ }
|
|
462
|
-
|
|
496
|
+
const cancelled = cancelSourceWorkItemForPrunedDispatch(entry, reason);
|
|
497
|
+
log('info', `Dropped stale PR dispatch ${entry.id}: ${reason}${cancelled ? ' (source work item cancelled)' : ''}`);
|
|
463
498
|
}
|
|
464
499
|
return removed.length;
|
|
465
500
|
}
|
package/engine/lifecycle.js
CHANGED
|
@@ -790,6 +790,19 @@ function reconcilePrdStatuses(config) {
|
|
|
790
790
|
|
|
791
791
|
// ─── PR Sync from Output ─────────────────────────────────────────────────────
|
|
792
792
|
|
|
793
|
+
const ONE_SHOT_CONTEXT_PR_TYPES = new Set([
|
|
794
|
+
WORK_TYPE.REVIEW,
|
|
795
|
+
WORK_TYPE.ASK,
|
|
796
|
+
WORK_TYPE.EXPLORE,
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
function shouldSyncPrAsContextOnly(meta) {
|
|
800
|
+
const item = meta?.item;
|
|
801
|
+
if (!item?.oneShot || item.skipPr) return false;
|
|
802
|
+
const type = String(item.type || item.itemType || '').trim();
|
|
803
|
+
return ONE_SHOT_CONTEXT_PR_TYPES.has(type);
|
|
804
|
+
}
|
|
805
|
+
|
|
793
806
|
function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
794
807
|
const { structuredCompletion = null } = opts;
|
|
795
808
|
const outputText = String(output || '');
|
|
@@ -996,13 +1009,14 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
996
1009
|
url: prUrl,
|
|
997
1010
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
998
1011
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
999
|
-
itemType: meta?.item?.itemType || ''
|
|
1012
|
+
itemType: meta?.item?.itemType || '',
|
|
1013
|
+
contextOnly: shouldSyncPrAsContextOnly(meta),
|
|
1000
1014
|
};
|
|
1001
1015
|
applyScheduleContextToPrEntry(entry, meta?.item, config);
|
|
1002
|
-
// Issue #1772: one-off dispatches (
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1016
|
+
// Issue #1772: explicit one-off review/ask/explore dispatches (for example,
|
|
1017
|
+
// "review this PR" via CC) must stay reference-only. QA-session setup and
|
|
1018
|
+
// other oneShot+skipPr PR-targeted work are operational context, not an
|
|
1019
|
+
// observe toggle, so they must not demote a real tracked PR.
|
|
1006
1020
|
newPrsByPath.get(prPath).entries.push({ prId, fullId, entry });
|
|
1007
1021
|
}
|
|
1008
1022
|
|
|
@@ -1145,7 +1159,7 @@ function _existingPrRecordForCanonicalId(canonicalPrId) {
|
|
|
1145
1159
|
* can render the real merged/abandoned status instead of a perpetual blue ○.
|
|
1146
1160
|
*
|
|
1147
1161
|
* Idempotent: returns `{ enrolled: false, reason: 'already_tracked' }` if a row
|
|
1148
|
-
* already exists. Marks new records with `
|
|
1162
|
+
* already exists. Marks new records with `contextOnly: true` so the engine
|
|
1149
1163
|
* doesn't try to manage them (no re-dispatch of fix/review loops). Live state
|
|
1150
1164
|
* is fetched via `gh pr view` for GitHub; ADO enrollment is best-effort with
|
|
1151
1165
|
* a conservative ACTIVE default.
|
|
@@ -1259,7 +1273,7 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1259
1273
|
url,
|
|
1260
1274
|
prdItems: opts.itemId ? [opts.itemId] : [],
|
|
1261
1275
|
_attachedAt: ts(),
|
|
1262
|
-
|
|
1276
|
+
contextOnly: true,
|
|
1263
1277
|
_enrolledBy: 'enrollPrFromCanonicalId',
|
|
1264
1278
|
};
|
|
1265
1279
|
if (liveState && liveState.mergedAt) entry.mergedAt = liveState.mergedAt;
|
package/engine/shared.js
CHANGED
|
@@ -6348,6 +6348,15 @@ function applyPrFieldDelta(target, before, after) {
|
|
|
6348
6348
|
function normalizePrRecord(pr, project = null) {
|
|
6349
6349
|
if (!pr || typeof pr !== 'object') return false;
|
|
6350
6350
|
let changed = false;
|
|
6351
|
+
const hasCanonicalContextOnly = Object.prototype.hasOwnProperty.call(pr, 'contextOnly');
|
|
6352
|
+
if (hasCanonicalContextOnly) {
|
|
6353
|
+
for (const legacyKey of ['_contextOnly', '_autoObserve', '_manual']) {
|
|
6354
|
+
if (Object.prototype.hasOwnProperty.call(pr, legacyKey)) {
|
|
6355
|
+
delete pr[legacyKey];
|
|
6356
|
+
changed = true;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6351
6360
|
const prNumber = getPrNumber(pr.prNumber ?? pr.id ?? pr.url);
|
|
6352
6361
|
if (prNumber != null && pr.prNumber !== prNumber) {
|
|
6353
6362
|
pr.prNumber = prNumber;
|
|
@@ -6386,17 +6395,19 @@ function normalizePrLinkItems(value) {
|
|
|
6386
6395
|
return [...new Set(items.filter(item => typeof item === 'string' && item))];
|
|
6387
6396
|
}
|
|
6388
6397
|
|
|
6389
|
-
// W-mq5s5ttx000j7ab8-a — canonical `contextOnly` gate
|
|
6390
|
-
//
|
|
6391
|
-
//
|
|
6392
|
-
//
|
|
6393
|
-
//
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
+
// W-mq5s5ttx000j7ab8-a / W-mqerisvz000n3901 — canonical `contextOnly` gate
|
|
6399
|
+
// with a legacy bridge. Canonical `contextOnly` wins whenever it exists; legacy
|
|
6400
|
+
// `_contextOnly` is only a fallback for pre-migration/raw records. This keeps
|
|
6401
|
+
// observe toggles (`contextOnly:false`) from being overridden by stale legacy
|
|
6402
|
+
// contamination while still treating old context-only rows as reference-only.
|
|
6403
|
+
function isContextOnlyPrRecord(pr) {
|
|
6404
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
6405
|
+
if (typeof pr.contextOnly === 'boolean') return pr.contextOnly;
|
|
6406
|
+
return pr._contextOnly === true;
|
|
6407
|
+
}
|
|
6408
|
+
|
|
6398
6409
|
function isAutoManagedPrRecord(pr) {
|
|
6399
|
-
return !!pr && typeof pr === 'object' && pr
|
|
6410
|
+
return !!pr && typeof pr === 'object' && !isContextOnlyPrRecord(pr);
|
|
6400
6411
|
}
|
|
6401
6412
|
|
|
6402
6413
|
function mergePrLinkItems(links, prId, itemIds) {
|
|
@@ -6548,6 +6559,13 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
6548
6559
|
prNumber: prNumber ?? entry.prNumber ?? null,
|
|
6549
6560
|
prdItems: linkedItemIds,
|
|
6550
6561
|
};
|
|
6562
|
+
const normalizedEntryContextOnly = normalizedEntry.contextOnly != null
|
|
6563
|
+
? normalizedEntry.contextOnly
|
|
6564
|
+
: normalizedEntry._contextOnly;
|
|
6565
|
+
if (normalizedEntryContextOnly != null) {
|
|
6566
|
+
normalizedEntry.contextOnly = normalizedEntryContextOnly === true;
|
|
6567
|
+
delete normalizedEntry._contextOnly;
|
|
6568
|
+
}
|
|
6551
6569
|
|
|
6552
6570
|
let created = false;
|
|
6553
6571
|
let linked = false;
|
|
@@ -6568,7 +6586,8 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
6568
6586
|
} else {
|
|
6569
6587
|
target.id = canonicalId;
|
|
6570
6588
|
if (prNumber != null) target.prNumber = prNumber;
|
|
6571
|
-
const targetWasAutoManaged = isAutoManagedPrRecord(target)
|
|
6589
|
+
const targetWasAutoManaged = isAutoManagedPrRecord(target)
|
|
6590
|
+
|| (target.contextOnly == null && target._contextOnly === true && _prRecordIsLegacyManaged(target));
|
|
6572
6591
|
for (const key of ['url', 'title', 'description', 'agent', 'branch', 'reviewStatus', 'status', 'created', 'sourcePlan', 'itemType']) {
|
|
6573
6592
|
if (normalizedEntry[key] != null && normalizedEntry[key] !== '' && (target[key] == null || target[key] === '')) {
|
|
6574
6593
|
target[key] = normalizedEntry[key];
|
|
@@ -6591,7 +6610,10 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
6591
6610
|
: normalizedEntry._contextOnly;
|
|
6592
6611
|
if (incomingContextOnly != null) {
|
|
6593
6612
|
const wouldDemoteManagedPr = incomingContextOnly === true && targetWasAutoManaged;
|
|
6594
|
-
|
|
6613
|
+
target.contextOnly = wouldDemoteManagedPr ? false : incomingContextOnly === true;
|
|
6614
|
+
delete target._contextOnly;
|
|
6615
|
+
delete target._autoObserve;
|
|
6616
|
+
delete target._manual;
|
|
6595
6617
|
}
|
|
6596
6618
|
}
|
|
6597
6619
|
target.prdItems = normalizePrLinkItems(target.prdItems || []);
|
|
@@ -8298,7 +8320,7 @@ module.exports = {
|
|
|
8298
8320
|
normalizePrRecords,
|
|
8299
8321
|
normalizePrLinkItems, // exported for testing
|
|
8300
8322
|
mergePrLinkItems, // exported for testing
|
|
8301
|
-
|
|
8323
|
+
isContextOnlyPrRecord,
|
|
8302
8324
|
upsertPullRequestRecord,
|
|
8303
8325
|
isAutoManagedPrRecord, // W-mq5s5ttx000j7ab8-a — exported for engine + watch-plugin gate consolidation
|
|
8304
8326
|
migratePrGateFlags, // W-mq5s5ttx000j7ab8-a — boot migration wired from engine/cli.js
|
package/engine.js
CHANGED
|
@@ -6043,7 +6043,7 @@ async function discoverFromPrs(config, project) {
|
|
|
6043
6043
|
|
|
6044
6044
|
for (const pr of prs) {
|
|
6045
6045
|
if (pr.status !== PR_STATUS.ACTIVE) continue;
|
|
6046
|
-
if (pr
|
|
6046
|
+
if (shared.isContextOnlyPrRecord(pr)) {
|
|
6047
6047
|
_logPrDispatchSkipOnce(pr, 'context-only');
|
|
6048
6048
|
continue;
|
|
6049
6049
|
}
|
|
@@ -6057,11 +6057,9 @@ async function discoverFromPrs(config, project) {
|
|
|
6057
6057
|
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${prBranchForMutex} locked by another agent`);
|
|
6058
6058
|
continue;
|
|
6059
6059
|
}
|
|
6060
|
-
// Auto-managed gate: single source of truth in engine/shared.js.
|
|
6061
|
-
//
|
|
6062
|
-
//
|
|
6063
|
-
// the prior inline `knownAgents.has(...) || prdItems || (_manual && !_contextOnly)`
|
|
6064
|
-
// silently dropped PRs with `_autoObserve: true` + human author.
|
|
6060
|
+
// Auto-managed gate: single source of truth in engine/shared.js. Canonical
|
|
6061
|
+
// `contextOnly` wins over stale legacy flags; legacy `_contextOnly` is only
|
|
6062
|
+
// a fallback for raw pre-migration records.
|
|
6065
6063
|
if (!shared.isAutoManagedPrRecord(pr)) {
|
|
6066
6064
|
_logPrDispatchSkipOnce(pr, 'not-auto-managed');
|
|
6067
6065
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2197",
|
|
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"
|