@yemi33/minions 0.1.2191 → 0.1.2193

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 CHANGED
@@ -484,7 +484,7 @@ function normalizePrTitleMatchText(value) {
484
484
 
485
485
  function findPrRecordReferencedByText(prs, text, project = null) {
486
486
  if (!Array.isArray(prs) || !String(text || '').trim()) return null;
487
- const explicitRef = extractPrRefFromText(text);
487
+ const explicitRef = shared.extractPrRefFromText(text);
488
488
  if (explicitRef) return shared.findPrRecord(prs, explicitRef, project);
489
489
  const normalizedText = normalizePrTitleMatchText(text);
490
490
  if (!normalizedText) return null;
@@ -3957,14 +3957,6 @@ function trimTrailingPrRefPunctuation(value) {
3957
3957
  return String(value || '').replace(/[),.;:]+$/g, '');
3958
3958
  }
3959
3959
 
3960
- // Thin wrapper over shared.extractPrRefFromText so callers in dashboard.js
3961
- // keep their existing import shape while the canonical regex + extraction
3962
- // logic lives in engine/shared.js (issue #2999 / W-mpx6i5kh000ac040).
3963
- function extractPrRefFromText(value) {
3964
- return shared.extractPrRefFromText(value);
3965
- }
3966
-
3967
-
3968
3960
  // ── Shared LLM call core — used by CC panel and doc modals ──────────────────
3969
3961
 
3970
3962
  // Session store for doc modals — keyed by filePath or title, persisted to disk.
@@ -7316,42 +7308,6 @@ const server = http.createServer(async (req, res) => {
7316
7308
  return jsonReply(res, 200, plans);
7317
7309
  }
7318
7310
 
7319
- async function handlePlansUnarchive(req, res) {
7320
- try {
7321
- const body = await readBody(req);
7322
- if (!body.file) return jsonReply(res, 400, { error: 'file required' });
7323
- const file = body.file;
7324
- if (file.includes('..') || file.includes('\0') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
7325
-
7326
- const isJson = file.endsWith('.json');
7327
- const targetDir = isJson ? PRD_DIR : PLANS_DIR;
7328
- const archiveDir = path.join(targetDir, 'archive');
7329
- const archivePath = path.join(archiveDir, file);
7330
-
7331
- if (!fs.existsSync(archivePath)) return jsonReply(res, 404, { error: 'File not found in archive' });
7332
- fs.renameSync(archivePath, path.join(targetDir, file));
7333
-
7334
- // If unarchiving a PRD .json, also unarchive its source .md plan
7335
- let unarchivedSource = null;
7336
- if (isJson) {
7337
- try {
7338
- const prd = safeJson(path.join(targetDir, file));
7339
- if (prd?.source_plan) {
7340
- const mdArchivePath = path.join(PLANS_DIR, 'archive', prd.source_plan);
7341
- if (fs.existsSync(mdArchivePath)) {
7342
- fs.renameSync(mdArchivePath, path.join(PLANS_DIR, prd.source_plan));
7343
- unarchivedSource = prd.source_plan;
7344
- }
7345
- }
7346
- } catch { /* optional */ }
7347
- }
7348
-
7349
- invalidateStatusCache();
7350
- invalidatePlansCache();
7351
- return jsonReply(res, 200, { ok: true, unarchivedSource });
7352
- } catch (e) { return jsonReply(res, 400, { error: e.message }); }
7353
- }
7354
-
7355
7311
  async function handlePlansArchiveRead(req, res, match) {
7356
7312
  const file = decodeURIComponent(match[1]);
7357
7313
  if (file.includes('..') || file.includes('\0')) return jsonReply(res, 400, { error: 'invalid' });
@@ -7681,6 +7637,39 @@ const server = http.createServer(async (req, res) => {
7681
7637
  if (!materializedPlanItemIds.has(pi.id)) newCount++;
7682
7638
  }
7683
7639
 
7640
+ // W-mqexsm7y000qccd2 — flip orphan-pending PRD items to 'missing' so
7641
+ // the materializer's PRD_MATERIALIZABLE filter picks them up on the
7642
+ // next tick. 'pending' isn't a valid PRD-item status (the enum is
7643
+ // {missing, updated, done}) but leaks into PRDs via upstream tools;
7644
+ // without this flip the operator sees `new: N` but the WIs never
7645
+ // appear because the materializer drops them. Defensive scope: only
7646
+ // flip items with no live WI (orphans) — pending PRD items that
7647
+ // already have a kept/dispatched WI are left alone so the existing
7648
+ // re-open path stays authoritative.
7649
+ let orphanPendingFlipped = 0;
7650
+ const orphanPendingIds = new Set();
7651
+ for (const pi of planItems) {
7652
+ if (pi.status === 'pending' && !materializedPlanItemIds.has(pi.id) && pi.id) {
7653
+ orphanPendingIds.add(pi.id);
7654
+ }
7655
+ }
7656
+ if (orphanPendingIds.size > 0) {
7657
+ try {
7658
+ mutateJsonFileLocked(planPath, (current) => {
7659
+ if (!current || !Array.isArray(current.missing_features)) return current;
7660
+ const stamp = new Date().toISOString();
7661
+ for (const f of current.missing_features) {
7662
+ if (orphanPendingIds.has(f.id) && f.status === 'pending') {
7663
+ f.status = 'missing';
7664
+ f._orphanPendingFlippedAt = stamp;
7665
+ orphanPendingFlipped++;
7666
+ }
7667
+ }
7668
+ return current;
7669
+ });
7670
+ } catch (e) { console.error('orphan-pending PRD flip:', e.message); }
7671
+ }
7672
+
7684
7673
  // Clean dispatch entries for deleted items
7685
7674
  for (const itemId of deletedItemIds) {
7686
7675
  cleanDispatchEntries(d =>
@@ -7688,7 +7677,7 @@ const server = http.createServer(async (req, res) => {
7688
7677
  );
7689
7678
  }
7690
7679
 
7691
- return jsonReply(res, 200, { ok: true, reset, kept, new: newCount });
7680
+ return jsonReply(res, 200, { ok: true, reset, kept, new: newCount, orphanPendingFlipped });
7692
7681
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
7693
7682
  }
7694
7683
 
package/docs/README.md CHANGED
@@ -52,6 +52,7 @@ Architecture, design proposals, and lifecycle references for people working on t
52
52
  Operational runbooks for engine operators and fleet maintainers.
53
53
 
54
54
  - [auto-discovery.md](auto-discovery.md) — Auto-discovery and execution pipeline: the per-tick orchestration loop and the four work-discovery sources.
55
+ - [diagnostics-memory.md](diagnostics-memory.md) — Operator runbook for the in-process memory + perf observability surface: `/api/diagnostics/memory[/history]`, `/api/diagnostics/heap-snapshot` guard-token capture, `MEMORY_BASELINE` log emissions, `--cpu-prof`/`--heap-prof` capture, and the `test/perf/soak.test.js` heap-growth regression gate.
55
56
  - [engine-restart.md](engine-restart.md) — How agents survive an engine restart: state persistence, the 20-minute startup grace period, and orphan reattachment via PID files and `live-output.log`.
56
57
  - [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
57
58
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base sweep runbook: how `engine/kb-sweep.js` consolidates `notes/inbox/` into `knowledge/` and survives `minions restart`.
@@ -1,6 +1,6 @@
1
1
  # Completion Reports
2
2
 
3
- Every Minions agent ends its dispatch by writing a JSON completion report. The report is the engine's primary, machine-readable signal that the work item is finished and is the source of truth for status, retry decisions, dashboard surfaces, and downstream automation. Fenced ` ```completion ` blocks in stdout are still parsed as a compatibility fallback, but the JSON file always wins when both exist.
3
+ Every Minions agent ends its dispatch by writing a JSON completion report. The report is the engine's sole structured-completion source — it is machine-readable, signed by a per-spawn nonce, and the source of truth for status, retry decisions, dashboard surfaces, and downstream automation. (The legacy fenced ` ```completion ` block and `task_complete` prose-summary fallbacks were removed in PR #126 / commit `ac76148f`; without a well-formed report on disk the engine has no structured signals to read.)
4
4
 
5
5
  This document is the canonical schema. Playbooks should cross-link here instead of restating the field list.
6
6
 
@@ -24,7 +24,7 @@ The agent must write the JSON to that exact path before exiting. Any character o
24
24
  Each spawn also receives a per-dispatch cryptographic value via the `MINIONS_COMPLETION_NONCE` environment variable. The engine generates this with `crypto.randomBytes(16).toString('hex')` in `engine.js:spawnAgent()` and stores it on the in-memory active-process record. The agent is required to copy the value verbatim into the report's `nonce` field. On parse, `engine/lifecycle.js:runPostCompletionHooks()` compares `report.nonce` against the in-memory value:
25
25
 
26
26
  - **Match** — the report is trusted and processed normally.
27
- - **Mismatch** — the report is treated as forged (a prompt-injected agent or a stale process writing into a sibling dispatch's completion path). Every signal it carries — `status`, `pr`, `noop`, `failure_class`, `retryable`, `needs_rerun`, fenced/summary fallbacks — is discarded. The dispatch is failed with `failure_class: 'completion-nonce-mismatch'` and the work item is marked failed (no auto-retry honors the agent's `retryable` claim).
27
+ - **Mismatch** — the report is treated as forged (a prompt-injected agent or a stale process writing into a sibling dispatch's completion path). Every signal it carries — `status`, `pr`, `noop`, `failure_class`, `retryable`, `needs_rerun`, artifacts, follow-ups — is discarded. The dispatch is failed with `failure_class: 'completion-nonce-mismatch'` and the work item is marked failed (no auto-retry honors the agent's `retryable` claim).
28
28
  - **Missing** — by default, the engine logs `[security] completion-nonce-missing dispatch=… required=false (degraded — report honored)` and still honors the report. Flip `ENGINE_DEFAULTS.completionNonceRequired` (or `engine.completionNonceRequired` in `config.json`) to `true` to hard-fail missing nonces too. Default is `false` for one release so older runtime caches and agents that haven't picked up the prompt change degrade with a warning instead of breaking.
29
29
 
30
30
  Security event log lines are emitted on the `error` channel and are designed to be greppable:
@@ -296,11 +296,10 @@ The dashboard caps the rendered list at 20 artifacts per report (`engine/queries
296
296
 
297
297
  The engine reads completion signals in this order (`engine/lifecycle.js`):
298
298
 
299
- 1. The JSON report at `MINIONS_COMPLETION_REPORT` — primary.
300
- 2. A fenced ` ```completion ` block in the agent's stdout fallback only.
301
- 3. Process exit code and stdout heuristics — last-resort recovery.
299
+ 1. The JSON report at `MINIONS_COMPLETION_REPORT` — sole structured-completion source. `parseCompletionReportFile()` requires a well-formed plain-object JSON with a `status` field; anything else is dropped with a warn log.
300
+ 2. Runtime result events + the `[process-exit]` sentinel in `live-output.log` — used to confirm the dispatch actually exited and to capture the runtime's own result prose into `resultSummary`. These cannot supply structured fields (`pr`, `failure_class`, `retryable`, `noop`, `verdict`, `artifacts`, …); without a report on disk those signals are simply absent.
302
301
 
303
- If the JSON report exists and is well-formed, the engine ignores the fenced block. If the JSON is missing or malformed (logged as a warning), the engine falls back to the fenced block, then to stdout parsing.
302
+ The legacy fenced ` ```completion ` block parser and the `task_complete` prose-summary fallback were removed in PR #126 (`ac76148f`, 2026-06-11) after a 14-day sweep window showed zero hits. If the report file is missing, malformed, or fails the nonce check, the engine logs at warn and the dispatch proceeds with no structured signals — the PR-attachment contract and the phantom-completion guard in `detectNonTerminalResultSummary` are the only remaining safety nets.
304
303
 
305
304
  ## Examples
306
305
 
package/engine.js CHANGED
@@ -3537,10 +3537,10 @@ async function spawnAgent(dispatchItem, config) {
3537
3537
  try { shared.killImmediate(proc); } catch { /* already exited */ }
3538
3538
  }
3539
3539
  if (registeredInActiveProcesses) {
3540
- try { activeProcesses.delete(id); } catch { /* map.delete never throws but be defensive */ }
3540
+ activeProcesses.delete(id);
3541
3541
  }
3542
3542
  if (registeredInActivityMap) {
3543
- try { realActivityMap.delete(id); } catch { /* defensive */ }
3543
+ realActivityMap.delete(id);
3544
3544
  }
3545
3545
  if (logFd !== undefined) {
3546
3546
  try { fs.closeSync(logFd); } catch { /* fd may already be closed */ }
@@ -5196,12 +5196,59 @@ function materializePlansAsWorkItems(config) {
5196
5196
  // the bumped name itself is owned by an archived PRD — same risk.
5197
5197
  log('warn', `Plan project enforcement: skipping rename of ${fileName} to ${toBasename} — would collide with archived PRD`);
5198
5198
  } else {
5199
+ // W-mqfevwrm — Serialize the rename behind file locks on BOTH the
5200
+ // source and destination paths so a concurrent `mutateJsonFileLocked`
5201
+ // on either name cannot interleave (creating a divergent ghost PRD
5202
+ // or losing data). Move the `.backup` sidecar along with the `.json`
5203
+ // so the canonical name retains its restore-from-backup safety net
5204
+ // — without this, deleting the renamed `.json` (concurrent sweep,
5205
+ // plan-completion purge, etc.) leaves no `.backup` under the new
5206
+ // name and the PRD is permanently lost (live incident 2026-06-15:
5207
+ // `minions-opg-2026-06-10.json` gone, only the OLD-name `.backup`
5208
+ // survived, all 9 WIs orphaned). The OLD-name `.backup` is removed
5209
+ // so it cannot resurrect a ghost PRD on a future read of the OLD
5210
+ // path (W-mouptdh1000h9f39-style landmine).
5199
5211
  try {
5200
- fs.renameSync(fromPath, toPath);
5201
- nextFileName = toBasename;
5202
- const migrated = migratePrdFilenameReferences(fileName, nextFileName);
5203
- if (migrated > 0) log('info', `Plan project enforcement: migrated ${migrated} PRD reference(s) from ${fileName} to ${nextFileName}`);
5204
- changed = true;
5212
+ withFileLock(`${fromPath}.lock`, () => {
5213
+ withFileLock(`${toPath}.lock`, () => {
5214
+ // Re-verify under both locks — another tick may have raced
5215
+ // ahead and either deleted the source or claimed the dest.
5216
+ if (!fs.existsSync(fromPath)) {
5217
+ log('warn', `Plan project enforcement: source ${fileName} disappeared before rename (raced)`);
5218
+ return;
5219
+ }
5220
+ if (fs.existsSync(toPath)) {
5221
+ log('warn', `Plan project enforcement: destination ${toBasename} appeared before rename (raced) — skipping`);
5222
+ return;
5223
+ }
5224
+ fs.renameSync(fromPath, toPath);
5225
+ // Move the `.backup` sidecar along with the primary `.json`
5226
+ // so safeJson's restore-from-backup path stays viable under
5227
+ // the new canonical name. Both ops are best-effort: missing
5228
+ // `.backup` is normal (nothing to move) and an unlink failure
5229
+ // on the OLD-name `.backup` is logged but non-fatal.
5230
+ const fromBackup = `${fromPath}.backup`;
5231
+ const toBackup = `${toPath}.backup`;
5232
+ if (fs.existsSync(fromBackup)) {
5233
+ try {
5234
+ fs.renameSync(fromBackup, toBackup);
5235
+ } catch (be) {
5236
+ // Fall back to copy + unlink if rename across the same dir
5237
+ // surfaces a Windows EPERM (AV/Search Indexer hold).
5238
+ try {
5239
+ fs.copyFileSync(fromBackup, toBackup);
5240
+ try { fs.unlinkSync(fromBackup); } catch { /* best-effort */ }
5241
+ } catch (ce) {
5242
+ log('warn', `Plan project enforcement: could not move .backup sidecar ${fileName}.backup → ${toBasename}.backup: ${be.message} / ${ce.message}`);
5243
+ }
5244
+ }
5245
+ }
5246
+ nextFileName = toBasename;
5247
+ const migrated = migratePrdFilenameReferences(fileName, nextFileName);
5248
+ if (migrated > 0) log('info', `Plan project enforcement: migrated ${migrated} PRD reference(s) from ${fileName} to ${nextFileName}`);
5249
+ changed = true;
5250
+ });
5251
+ });
5205
5252
  } catch (e) {
5206
5253
  log('warn', `Plan project enforcement: could not rename ${fileName} to ${toBasename}: ${e.message}`);
5207
5254
  }
@@ -5369,9 +5416,18 @@ function materializePlansAsWorkItems(config) {
5369
5416
  for (const w of queries.getWorkItems()) {
5370
5417
  if (w.id) allExistingWiIds.add(w.id);
5371
5418
  }
5419
+ // W-mqexsm7y000qccd2 — orphan-pending PRD items (status === 'pending'
5420
+ // with no live WI) used to be silently ignored: 'pending' isn't in
5421
+ // PRD_MATERIALIZABLE ({missing, updated}) and isn't a DONE_STATUSES
5422
+ // member, so the materializer's filter dropped them and dependents
5423
+ // stayed dependency_unmet forever. handlePlansRegenerate flips these
5424
+ // to 'missing' on operator action; the materializer extension below
5425
+ // self-heals on every tick so the materializer is robust to PRD-item
5426
+ // status drift regardless of how 'pending' leaked in.
5372
5427
  const items = plan.missing_features.filter(f =>
5373
5428
  statusFilter.has(f.status) ||
5374
- (DONE_STATUSES.has(f.status) && f.id && !allExistingWiIds.has(f.id))
5429
+ (DONE_STATUSES.has(f.status) && f.id && !allExistingWiIds.has(f.id)) ||
5430
+ (f.status === 'pending' && f.id && !allExistingWiIds.has(f.id))
5375
5431
  );
5376
5432
 
5377
5433
  // Group items by target project (per-item project field overrides plan-level project)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2191",
3
+ "version": "0.1.2193",
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"