@yemi33/minions 0.1.2296 → 0.1.2298
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/pages/home.html +3 -3
- package/docs/completion-reports.md +1 -0
- package/engine/dispatch.js +31 -0
- package/engine/lifecycle.js +42 -17
- package/engine.js +12 -10
- package/package.json +1 -1
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
</section>
|
|
18
18
|
<section>
|
|
19
|
-
<h2>Minions Members <span
|
|
19
|
+
<h2>Minions Members <span class="qa-section-subtitle">click for details</span></h2>
|
|
20
20
|
<div class="agents" id="agents-grid">Loading...</div>
|
|
21
21
|
</section>
|
|
22
22
|
<section>
|
|
23
|
-
<h2>Pinned Context <button class="btn-add" style="margin-left:8px" onclick="openPinNoteModal()">+ Pin</button> <span
|
|
23
|
+
<h2>Pinned Context <button class="btn-add" style="margin-left:8px" onclick="openPinNoteModal()">+ Pin</button> <span class="qa-section-subtitle">injected into all agent prompts</span></h2>
|
|
24
24
|
<div id="pinned-content"><p class="empty">No pinned notes.</p></div>
|
|
25
25
|
</section>
|
|
26
26
|
<section>
|
|
27
|
-
<h2>Dispatch Queue <span
|
|
27
|
+
<h2>Dispatch Queue <span class="qa-section-subtitle">agents being assigned and completing tasks</span></h2>
|
|
28
28
|
<div class="dispatch-stats" id="dispatch-stats"></div>
|
|
29
29
|
<div id="dispatch-active"></div>
|
|
30
30
|
<div id="dispatch-pending"></div>
|
|
@@ -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/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]);
|
|
@@ -5426,6 +5447,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5426
5447
|
meta._noopReason = noopRationale.slice(0, 500);
|
|
5427
5448
|
}
|
|
5428
5449
|
updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
5450
|
+
// P-mqyp000ab028c9d0 — persist affected_files from the completion report
|
|
5451
|
+
// onto the work item so future dispatches of the same WI can participate in
|
|
5452
|
+
// the file-overlap conflict detection in dispatch.js#_warnFileOverlap.
|
|
5453
|
+
try { storeAffectedFilesFromCompletion(meta, structuredCompletion); } catch (err) { log('warn', `storeAffectedFilesFromCompletion: ${err.message}`); }
|
|
5429
5454
|
// W-mqtplpk6001oe6d5 — back-stamp workItemId onto the PRD item now that WI is done.
|
|
5430
5455
|
if (meta.item.sourcePlan) {
|
|
5431
5456
|
try { stampPrdItemWorkItemId(meta.item.id, meta.item.sourcePlan); } catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
package/engine.js
CHANGED
|
@@ -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
|
*
|
|
@@ -6198,6 +6189,17 @@ function materializePlansAsWorkItems(config) {
|
|
|
6198
6189
|
|
|
6199
6190
|
let totalCreated = 0;
|
|
6200
6191
|
for (const [projName, { project, items: projItems }] of itemsByProject) {
|
|
6192
|
+
// Re-read PRD status from disk to guard against mid-tick status changes (P-f1a3b5c7).
|
|
6193
|
+
// A pause/reject/awaiting-approval signal may arrive between the outer-loop initial
|
|
6194
|
+
// read and this point (e.g. operator pauses via dashboard while the tick is running).
|
|
6195
|
+
// Re-reading here prevents phantom WI creation on a paused, rejected, or unapproved plan.
|
|
6196
|
+
const freshPrd = safeJsonNoRestore(path.join(PRD_DIR, file));
|
|
6197
|
+
const freshStatus = (freshPrd || {}).status || ((freshPrd || {}).requires_approval ? 'awaiting-approval' : null);
|
|
6198
|
+
if (freshStatus === PLAN_STATUS.PAUSED || freshStatus === PLAN_STATUS.REJECTED || freshStatus === 'awaiting-approval') {
|
|
6199
|
+
log('warn', `PRD ${file}: status changed to '${freshStatus}' mid-tick — skipping WI batch for ${projName} (P-f1a3b5c7)`);
|
|
6200
|
+
break;
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6201
6203
|
const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
|
|
6202
6204
|
let created = 0;
|
|
6203
6205
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
@@ -10559,7 +10561,7 @@ module.exports = {
|
|
|
10559
10561
|
|
|
10560
10562
|
// Shared helpers (used by lifecycle.js and tests)
|
|
10561
10563
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
10562
|
-
safePrdProjectSlug,
|
|
10564
|
+
safePrdProjectSlug, // exported for testing (W-mqyn7joo0004079f)
|
|
10563
10565
|
isSoftFixDispatch, // exported for testing (W-mqyn7joo0004079f)
|
|
10564
10566
|
areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
|
|
10565
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.2298",
|
|
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"
|