@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.
@@ -16,15 +16,15 @@
16
16
  </div>
17
17
  </section>
18
18
  <section>
19
- <h2>Minions Members <span style="font-size:var(--text-sm);color:var(--border);font-weight:400;text-transform:none;letter-spacing:0">click for details</span></h2>
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 style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">injected into all agent prompts</span></h2>
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 style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">agents being assigned and completing tasks</span></h2>
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). |
@@ -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;
@@ -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
- // Archive PRD .json to prd/archive/
499
- const prdArchiveDir = path.join(PRD_DIR, 'archive');
500
- if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
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
- // DATA-LOSS GUARD: moveFileNoClobber dedupes + retries so a same-basename
504
- // collision can't silently overwrite a previously-archived PRD (+ its
505
- // completed work-item history). renameSync clobbers on Windows.
506
- const prdDest = shared.moveFileNoClobber(planPath, prdArchiveDir, planFile);
507
- log('info', `Archived completed PRD: prd/archive/${path.basename(prdDest)}`);
508
- }
509
- // Remove .backup sidecar — if left behind, safeJson() would restore the pre-completion
510
- // snapshot (status: approved, no _completionNotified) on engine restart, re-triggering
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, safePrdFilenameForProject, // exported for testing (W-mqyn7joo0004079f)
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.2296",
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"