@yemi33/minions 0.1.2297 → 0.1.2299
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 +4 -10
- package/docs/completion-reports.md +1 -0
- package/engine/ado.js +0 -9
- package/engine/dispatch.js +31 -0
- package/engine/lifecycle.js +55 -29
- package/engine.js +4 -13
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -11444,25 +11444,19 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11444
11444
|
|
|
11445
11445
|
// W-mq03l6zh0006f0a1-d — read-only diagnostics surface for the per-org ADO
|
|
11446
11446
|
// throttle tracker. Returns { orgs: { [orgBase]: { throttled, retryAfter,
|
|
11447
|
-
// consecutiveHits } } }.
|
|
11448
|
-
//
|
|
11449
|
-
// process-global ado.getAdoThrottleState() under the synthetic key `global`
|
|
11450
|
-
// when the per-org getter is not present, so the endpoint stays live across
|
|
11451
|
-
// the staged rollout of the per-org isolation work.
|
|
11447
|
+
// consecutiveHits } } }. Uses getAdoThrottleState().perOrg, the per-org map
|
|
11448
|
+
// exposed by the aggregate getter (W-mq03l6zh0006f0a1-b).
|
|
11452
11449
|
async function handleDiagnosticsAdoThrottle(req, res) {
|
|
11453
11450
|
try {
|
|
11454
11451
|
let orgs = {};
|
|
11455
|
-
if (typeof ado.
|
|
11456
|
-
const all = ado.
|
|
11452
|
+
if (typeof ado.getAdoThrottleState === 'function') {
|
|
11453
|
+
const all = ado.getAdoThrottleState().perOrg || {};
|
|
11457
11454
|
// Defensive copy — handler must never expose internal mutable state.
|
|
11458
11455
|
for (const [k, v] of Object.entries(all)) {
|
|
11459
11456
|
if (v && typeof v === 'object') {
|
|
11460
11457
|
orgs[k] = { throttled: !!v.throttled, retryAfter: Number(v.retryAfter) || 0, consecutiveHits: Number(v.consecutiveHits) || 0 };
|
|
11461
11458
|
}
|
|
11462
11459
|
}
|
|
11463
|
-
} else if (typeof ado.getAdoThrottleState === 'function') {
|
|
11464
|
-
const v = ado.getAdoThrottleState() || {};
|
|
11465
|
-
orgs.global = { throttled: !!v.throttled, retryAfter: Number(v.retryAfter) || 0, consecutiveHits: Number(v.consecutiveHits) || 0 };
|
|
11466
11460
|
}
|
|
11467
11461
|
return jsonReply(res, 200, { orgs });
|
|
11468
11462
|
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
@@ -81,6 +81,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
81
81
|
| `noop` | boolean | Canonical no-op signal. See [No-op semantics](#no-op-semantics). |
|
|
82
82
|
| `noopReason` | string | Human-readable rationale shown when `noop: true`. Falls back to `summary` if absent. |
|
|
83
83
|
| `files_changed` | string \| array | Comma-separated list (or array) of key files changed. |
|
|
84
|
+
| `affected_files` | string[] | Optional array of relative file paths this dispatch touched or plans to touch. Used by the dispatcher to emit a conflict warning (`WI <new-id> may conflict with in-progress <existing-id> on files: [list]`) when a new WI's `affected_files` overlaps with an in-progress WI's `affected_files`. Logging-only — dispatch is never blocked. Stored back onto the work item after completion so re-dispatches can also participate in overlap detection. |
|
|
84
85
|
| `tests` | string | `pass`, `fail`, `skipped`, `N/A`, or a free-form note like `skipped — relying on PR pipeline`. |
|
|
85
86
|
| `pending` | string | Any remaining work, or `none`. |
|
|
86
87
|
| `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). |
|
package/engine/ado.js
CHANGED
|
@@ -2683,14 +2683,6 @@ const getAdoThrottleState = (orgBase) => {
|
|
|
2683
2683
|
return { throttled, retryAfter, consecutiveHits, perOrg };
|
|
2684
2684
|
};
|
|
2685
2685
|
|
|
2686
|
-
/** Returns the per-org tracker state map keyed by canonical orgBase. */
|
|
2687
|
-
const getAdoThrottleStateAll = () => {
|
|
2688
|
-
const out = {};
|
|
2689
|
-
for (const [key, tracker] of _adoThrottlesByOrg) {
|
|
2690
|
-
out[key] = tracker.getState();
|
|
2691
|
-
}
|
|
2692
|
-
return out;
|
|
2693
|
-
};
|
|
2694
2686
|
|
|
2695
2687
|
/**
|
|
2696
2688
|
* Query ADO for an open PR on a specific branch.
|
|
@@ -2955,7 +2947,6 @@ module.exports = {
|
|
|
2955
2947
|
_setAdoTokenAuthNoteWriterForTest, // exported for testing
|
|
2956
2948
|
isAdoThrottled,
|
|
2957
2949
|
getAdoThrottleState,
|
|
2958
|
-
getAdoThrottleStateAll,
|
|
2959
2950
|
fetchAdoPrMetadata,
|
|
2960
2951
|
prExists, // issue #246 — confirm a loose ref points at a PR (not a work item) before stamping
|
|
2961
2952
|
fetchSinglePrBuildStatus,
|
package/engine/dispatch.js
CHANGED
|
@@ -166,6 +166,36 @@ function getBranchDispatchLockKey(entry) {
|
|
|
166
166
|
return `${projectKey}:${normalizedBranch}`;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// ─── File-Overlap Conflict Detection (P-mqyp000ab028c9d0) ───────────────────
|
|
170
|
+
//
|
|
171
|
+
// Logging-only warning surfaced when a new WI declares affected_files that
|
|
172
|
+
// overlap with files declared by a currently in-progress (active) WI. No
|
|
173
|
+
// blocking — dispatch always proceeds. Goal: surface silent parallel edit
|
|
174
|
+
// races before they become merge conflicts.
|
|
175
|
+
|
|
176
|
+
function _getItemAffectedFiles(dispatchItem) {
|
|
177
|
+
const files = dispatchItem?.meta?.item?.affected_files;
|
|
178
|
+
if (!Array.isArray(files) || files.length === 0) return null;
|
|
179
|
+
const set = new Set(files.filter(f => typeof f === 'string' && f.length > 0));
|
|
180
|
+
return set.size > 0 ? set : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Called inside addToDispatch (before push) to emit one log line per active
|
|
184
|
+
// dispatch that shares at least one file with the incoming item.
|
|
185
|
+
function _warnFileOverlap(newItem, activeDispatches) {
|
|
186
|
+
const newFiles = _getItemAffectedFiles(newItem);
|
|
187
|
+
if (!newFiles) return;
|
|
188
|
+
const newWiId = newItem.meta?.item?.id || newItem.id;
|
|
189
|
+
for (const existing of activeDispatches) {
|
|
190
|
+
const existingFiles = _getItemAffectedFiles(existing);
|
|
191
|
+
if (!existingFiles) continue;
|
|
192
|
+
const overlap = [...newFiles].filter(f => existingFiles.has(f));
|
|
193
|
+
if (overlap.length === 0) continue;
|
|
194
|
+
const existingWiId = existing.meta?.item?.id || existing.id;
|
|
195
|
+
log('warn', `WI ${newWiId} may conflict with in-progress ${existingWiId} on files: ${overlap.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
169
199
|
function findActivePrOrBranchLock(dispatch, item) {
|
|
170
200
|
const active = dispatch.active || [];
|
|
171
201
|
|
|
@@ -238,6 +268,7 @@ function addToDispatch(item) {
|
|
|
238
268
|
log('info', `Dedup: skipping ${item.id} — ${activeLock.reason} already in ${activeLock.existing.id}`);
|
|
239
269
|
return dispatch;
|
|
240
270
|
}
|
|
271
|
+
_warnFileOverlap(item, dispatch.active || []);
|
|
241
272
|
dispatch.pending.push(item);
|
|
242
273
|
added = true;
|
|
243
274
|
return dispatch;
|
package/engine/lifecycle.js
CHANGED
|
@@ -495,25 +495,25 @@ function checkPlanCompletion(meta, config) {
|
|
|
495
495
|
function archivePlan(planFile, plan, projects, config) {
|
|
496
496
|
const planPath = path.join(PRD_DIR, planFile);
|
|
497
497
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
// Phase 10 — archive the PRD IN PLACE: flag it (archived/status/archivedAt)
|
|
499
|
+
// where it sits, rather than MOVING it to prd/archive/. This matches the
|
|
500
|
+
// canonical dashboard (_archivePrdPostProcess, #533) + watch-action archive
|
|
501
|
+
// paths. The legacy move was the LAST path still physically relocating a PRD,
|
|
502
|
+
// and it reintroduced the live↔archive basename-collision class (footgun #7)
|
|
503
|
+
// that the in-place flag model retired — plus it needed a .backup neutralize
|
|
504
|
+
// that is moot now that prd/*.json never gets a .backup sidecar at all (#570).
|
|
505
|
+
// mutateJsonFileLocked dual-writes the flag into SQL via the chokepoint; the
|
|
506
|
+
// archived-PRD scanner guards (#530) already treat status:'archived' correctly.
|
|
501
507
|
try {
|
|
502
508
|
if (fs.existsSync(planPath)) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// plan completion and spawning duplicate verify tasks for already-archived plans.
|
|
512
|
-
// On Windows, the unlink can fail due to file locking; overwrite with archived status
|
|
513
|
-
// as a fallback so a restored backup is inert even if deletion fails.
|
|
514
|
-
const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
|
|
515
|
-
if (!backupCleanup.ok) {
|
|
516
|
-
log('warn', `Archive backup cleanup failed for ${planFile}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`);
|
|
509
|
+
mutateJsonFileLocked(planPath, (data) => {
|
|
510
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
511
|
+
data.status = 'archived';
|
|
512
|
+
data.archived = true;
|
|
513
|
+
data.archivedAt = new Date().toISOString();
|
|
514
|
+
return data;
|
|
515
|
+
}, { defaultValue: {} });
|
|
516
|
+
log('info', `Archived PRD in place: ${planFile} (archived flag set)`);
|
|
517
517
|
}
|
|
518
518
|
} catch (err) {
|
|
519
519
|
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
@@ -777,6 +777,27 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
777
777
|
syncPrdItemStatus(itemId, status, meta.item?.sourcePlan);
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// P-mqyp000ab028c9d0 — persist affected_files from a completion report onto the
|
|
781
|
+
// work item for future file-overlap checks. Runs only when the completion report
|
|
782
|
+
// declares a non-empty string[] for `affected_files`; no-ops otherwise.
|
|
783
|
+
function storeAffectedFilesFromCompletion(meta, structuredCompletion) {
|
|
784
|
+
const raw = structuredCompletion?.affected_files;
|
|
785
|
+
if (!Array.isArray(raw) || raw.length === 0) return;
|
|
786
|
+
const files = raw.filter(f => typeof f === 'string' && f.length > 0);
|
|
787
|
+
if (files.length === 0) return;
|
|
788
|
+
const itemId = meta?.item?.id;
|
|
789
|
+
if (!itemId) return;
|
|
790
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
791
|
+
if (!wiPath) return;
|
|
792
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
793
|
+
if (!Array.isArray(items)) return items;
|
|
794
|
+
const target = items.find(i => i.id === itemId);
|
|
795
|
+
if (!target) return items;
|
|
796
|
+
target.affected_files = files;
|
|
797
|
+
return items;
|
|
798
|
+
}, { skipWriteIfUnchanged: true });
|
|
799
|
+
}
|
|
800
|
+
|
|
780
801
|
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
781
802
|
// (#984) PRD statuses that are stale when the work item is actually done
|
|
782
803
|
const _STALE_PRD_STATUSES = new Set([WI_STATUS.DISPATCHED, WI_STATUS.FAILED, WI_STATUS.PENDING]);
|
|
@@ -3586,24 +3607,25 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
3586
3607
|
function updateAgentHistory(agentId, dispatchItem, result) {
|
|
3587
3608
|
|
|
3588
3609
|
const historyPath = path.join(AGENTS_DIR, agentId, 'history.md');
|
|
3589
|
-
let history = safeRead(historyPath) || '# Agent History\n\n';
|
|
3590
3610
|
const entry = `### ${ts()} — ${result}\n` +
|
|
3591
3611
|
`- **Task:** ${dispatchItem.task}\n` +
|
|
3592
3612
|
`- **Type:** ${dispatchItem.type}\n` +
|
|
3593
3613
|
`- **Project:** ${dispatchItem.meta?.project?.name || 'central'}\n` +
|
|
3594
3614
|
`- **Branch:** ${dispatchItem.meta?.branch || 'none'}\n` +
|
|
3595
3615
|
`- **Dispatch ID:** ${dispatchItem.id}\n\n`;
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3616
|
+
shared.mutateTextFileLocked(historyPath, (history) => {
|
|
3617
|
+
history = history || '# Agent History\n\n';
|
|
3618
|
+
const headerEnd = history.indexOf('\n\n');
|
|
3619
|
+
if (headerEnd >= 0) {
|
|
3620
|
+
history = history.slice(0, headerEnd + 2) + entry + history.slice(headerEnd + 2);
|
|
3621
|
+
} else {
|
|
3622
|
+
history += entry;
|
|
3623
|
+
}
|
|
3624
|
+
const entries = history.split('### ').filter(Boolean);
|
|
3625
|
+
const header = entries[0].startsWith('#') ? entries.shift() : '# Agent History\n\n';
|
|
3626
|
+
const trimmed = entries.slice(0, 20);
|
|
3627
|
+
return header + trimmed.map(e => '### ' + e).join('');
|
|
3628
|
+
}, { defaultValue: '# Agent History\n\n' });
|
|
3607
3629
|
log('info', `Updated history for ${agentId}`);
|
|
3608
3630
|
}
|
|
3609
3631
|
|
|
@@ -5426,6 +5448,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5426
5448
|
meta._noopReason = noopRationale.slice(0, 500);
|
|
5427
5449
|
}
|
|
5428
5450
|
updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
5451
|
+
// P-mqyp000ab028c9d0 — persist affected_files from the completion report
|
|
5452
|
+
// onto the work item so future dispatches of the same WI can participate in
|
|
5453
|
+
// the file-overlap conflict detection in dispatch.js#_warnFileOverlap.
|
|
5454
|
+
try { storeAffectedFilesFromCompletion(meta, structuredCompletion); } catch (err) { log('warn', `storeAffectedFilesFromCompletion: ${err.message}`); }
|
|
5429
5455
|
// W-mqtplpk6001oe6d5 — back-stamp workItemId onto the PRD item now that WI is done.
|
|
5430
5456
|
if (meta.item.sourcePlan) {
|
|
5431
5457
|
try { stampPrdItemWorkItemId(meta.item.id, meta.item.sourcePlan); } catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
package/engine.js
CHANGED
|
@@ -5661,7 +5661,7 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
5661
5661
|
// ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
|
|
5662
5662
|
|
|
5663
5663
|
const { consolidateInbox } = require('./engine/consolidation');
|
|
5664
|
-
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled,
|
|
5664
|
+
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled, getAdoThrottleState } = require('./engine/ado');
|
|
5665
5665
|
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, checkLiveBuildAndConflict: ghCheckLiveBuildAndConflict, isGhThrottled } = require('./engine/github');
|
|
5666
5666
|
const { reconcileSharedBranchPrs } = require('./engine/shared-branch-pr-reconcile');
|
|
5667
5667
|
|
|
@@ -5762,15 +5762,6 @@ function safePrdProjectSlug(projectName) {
|
|
|
5762
5762
|
return slug || 'project';
|
|
5763
5763
|
}
|
|
5764
5764
|
|
|
5765
|
-
function safePrdFilenameForProject(projectName, suffix) {
|
|
5766
|
-
const fileName = `${safePrdProjectSlug(projectName)}-${suffix}.json`;
|
|
5767
|
-
const resolved = shared.sanitizePath(fileName, PRD_DIR);
|
|
5768
|
-
if (path.dirname(resolved) !== path.resolve(PRD_DIR)) {
|
|
5769
|
-
throw new Error('invalid PRD filename: nested paths are not allowed');
|
|
5770
|
-
}
|
|
5771
|
-
return path.basename(resolved);
|
|
5772
|
-
}
|
|
5773
|
-
|
|
5774
5765
|
/**
|
|
5775
5766
|
* Atomically reserve a unique PRD filename in `prdDir` (P-9b7e5d3c).
|
|
5776
5767
|
*
|
|
@@ -9779,7 +9770,7 @@ async function tickInner() {
|
|
|
9779
9770
|
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
9780
9771
|
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
9781
9772
|
// with one log line to avoid the per-project iteration cost.
|
|
9782
|
-
const adoThrottleStates =
|
|
9773
|
+
const adoThrottleStates = getAdoThrottleState().perOrg || {};
|
|
9783
9774
|
const adoOrgCount = Object.keys(adoThrottleStates).length;
|
|
9784
9775
|
const allAdoThrottled = adoOrgCount > 0 && Object.values(adoThrottleStates).every(s => s && s.throttled);
|
|
9785
9776
|
if (allAdoThrottled) {
|
|
@@ -9859,7 +9850,7 @@ async function tickInner() {
|
|
|
9859
9850
|
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
9860
9851
|
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
9861
9852
|
// with one log line to avoid the per-project iteration cost.
|
|
9862
|
-
const adoThrottleStates =
|
|
9853
|
+
const adoThrottleStates = getAdoThrottleState().perOrg || {};
|
|
9863
9854
|
const adoOrgCount = Object.keys(adoThrottleStates).length;
|
|
9864
9855
|
const allAdoThrottled = adoOrgCount > 0 && Object.values(adoThrottleStates).every(s => s && s.throttled);
|
|
9865
9856
|
if (allAdoThrottled) {
|
|
@@ -10570,7 +10561,7 @@ module.exports = {
|
|
|
10570
10561
|
|
|
10571
10562
|
// Shared helpers (used by lifecycle.js and tests)
|
|
10572
10563
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
10573
|
-
safePrdProjectSlug,
|
|
10564
|
+
safePrdProjectSlug, // exported for testing (W-mqyn7joo0004079f)
|
|
10574
10565
|
isSoftFixDispatch, // exported for testing (W-mqyn7joo0004079f)
|
|
10575
10566
|
areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
|
|
10576
10567
|
parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2299",
|
|
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"
|