@yemi33/minions 0.1.2192 → 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 +35 -10
- package/docs/README.md +1 -0
- package/docs/completion-reports.md +5 -6
- package/engine.js +64 -8
- package/package.json +1 -1
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.
|
|
@@ -7645,6 +7637,39 @@ const server = http.createServer(async (req, res) => {
|
|
|
7645
7637
|
if (!materializedPlanItemIds.has(pi.id)) newCount++;
|
|
7646
7638
|
}
|
|
7647
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
|
+
|
|
7648
7673
|
// Clean dispatch entries for deleted items
|
|
7649
7674
|
for (const itemId of deletedItemIds) {
|
|
7650
7675
|
cleanDispatchEntries(d =>
|
|
@@ -7652,7 +7677,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7652
7677
|
);
|
|
7653
7678
|
}
|
|
7654
7679
|
|
|
7655
|
-
return jsonReply(res, 200, { ok: true, reset, kept, new: newCount });
|
|
7680
|
+
return jsonReply(res, 200, { ok: true, reset, kept, new: newCount, orphanPendingFlipped });
|
|
7656
7681
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
7657
7682
|
}
|
|
7658
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
|
|
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`,
|
|
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` —
|
|
300
|
-
2.
|
|
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
|
-
|
|
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
|
-
|
|
3540
|
+
activeProcesses.delete(id);
|
|
3541
3541
|
}
|
|
3542
3542
|
if (registeredInActivityMap) {
|
|
3543
|
-
|
|
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
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
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.
|
|
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"
|