@yemi33/minions 0.1.2370 → 0.1.2371
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/bin/minions.js +38 -11
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +12 -27
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +80 -257
- package/docs/README.md +2 -3
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +3 -3
- package/docs/completion-reports.md +0 -121
- package/docs/copilot-cli-schema.md +9 -17
- package/docs/deprecated.json +0 -51
- package/docs/design-state-storage.md +4 -4
- package/docs/harness-propagation.md +33 -263
- package/docs/human-vs-automated.md +1 -1
- package/docs/named-agents.md +0 -2
- package/docs/plan-lifecycle.md +5 -6
- package/docs/qa-runbook-lifecycle.md +10 -25
- package/docs/shared-lifecycle-module-map.md +2 -10
- package/engine/ado-comment.js +5 -11
- package/engine/ado.js +10 -5
- package/engine/cleanup.js +16 -36
- package/engine/cli.js +13 -175
- package/engine/comment-format.js +8 -182
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/018-sql-only-cutover.js +56 -0
- package/engine/dispatch-store.js +0 -114
- package/engine/dispatch.js +1 -6
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +58 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-store.js +8 -6
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +8 -196
- package/engine/prd-store.js +115 -141
- package/engine/preflight.js +8 -55
- package/engine/projects.js +34 -41
- package/engine/pull-requests-store.js +9 -234
- package/engine/qa-runs.js +4 -15
- package/engine/qa-sessions.js +5 -17
- package/engine/queries.js +23 -103
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +1 -2
- package/engine/runtimes/codex.js +0 -2
- package/engine/runtimes/copilot.js +1 -4
- package/engine/shared-branch-pr-reconcile.js +2 -5
- package/engine/shared.js +174 -533
- package/engine/small-state-store.js +12 -647
- package/engine/spawn-agent.js +5 -96
- package/engine/state-operations.js +166 -0
- package/engine/supervisor.js +0 -1
- package/engine/watch-actions.js +2 -3
- package/engine/watches-store.js +2 -128
- package/engine/work-items-store.js +3 -254
- package/engine.js +102 -334
- package/package.json +2 -2
- package/playbooks/build-fix-complex.md +0 -4
- package/playbooks/fix.md +0 -4
- package/playbooks/implement-shared.md +0 -4
- package/playbooks/implement.md +0 -4
- package/playbooks/plan-to-prd.md +16 -11
- package/playbooks/plan.md +0 -4
- package/playbooks/review.md +0 -6
- package/playbooks/shared-rules.md +0 -65
- package/docs/harness-transparency.md +0 -199
- package/docs/project-skills.md +0 -193
- package/engine/discover-project-skills.js +0 -673
- package/engine/playbook-intents.js +0 -76
package/engine/cli.js
CHANGED
|
@@ -247,10 +247,10 @@ const CLI_COMMAND_DOCS = Object.freeze({
|
|
|
247
247
|
kill: { args: '', summary: 'Kill all active agents, reset to pending' },
|
|
248
248
|
complete: { args: '<dispatch-id>', summary: 'Mark a dispatch as done' },
|
|
249
249
|
cleanup: { args: '', summary: 'Clean temp files, worktrees, zombies' },
|
|
250
|
-
'mcp-sync': { args: '', summary: 'Print harness
|
|
251
|
-
doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print harness
|
|
250
|
+
'mcp-sync': { args: '', summary: 'Print runtime harness inventory (same source as `minions doctor --harness`; read-only, no writes)' },
|
|
251
|
+
doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print runtime harness inventory)' },
|
|
252
252
|
config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
|
|
253
|
-
pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--
|
|
253
|
+
pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh (ADO: --resolved posts pre-closed)' },
|
|
254
254
|
bridge: { args: 'status|health|enable|disable', summary: 'Constellation bridge: toggle and inspect the read-only cross-repo feed' },
|
|
255
255
|
});
|
|
256
256
|
|
|
@@ -413,111 +413,6 @@ function _applyRuntimeFlags({ cli, model, modelExplicit }) {
|
|
|
413
413
|
return { warnings, applied: true };
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
// resolveHarnessUsedForComment(flags) — P-7a3c9e21. Turn the `minions pr comment`
|
|
417
|
-
// harness flags into a grounded `harnessUsed` record suitable for the posters'
|
|
418
|
-
// `harnessUsed` param (engine/gh-comment.js / engine/ado-comment.js), which fold
|
|
419
|
-
// it via buildMinionsCommentBody -> buildHarnessUsedSection.
|
|
420
|
-
//
|
|
421
|
-
// Source precedence: `--harness-file <path>` (JSON on disk) wins over the inline
|
|
422
|
-
// `--harness-json <json>` convenience; neither supplied -> returns undefined so
|
|
423
|
-
// the comment body is byte-identical to today (no section). Read/parse failures
|
|
424
|
-
// are HARD errors (process.exit(2) with the offending message) — never a silent
|
|
425
|
-
// drop. Grounding uses the spawn-time `_harnessPropagated` manifest located by
|
|
426
|
-
// dispatch id (explicit `--dispatch-id` -> else basename(MINIONS_COMPLETION_REPORT,
|
|
427
|
-
// '.json')); an unresolvable manifest passes null, marking every entry
|
|
428
|
-
// grounded:false — the honest "no record" state, not a fatal condition.
|
|
429
|
-
function resolveHarnessUsedForComment(flags) {
|
|
430
|
-
const fileArg = flags['harness-file'];
|
|
431
|
-
const jsonArg = flags['harness-json'];
|
|
432
|
-
|
|
433
|
-
let raw;
|
|
434
|
-
if (fileArg !== undefined) {
|
|
435
|
-
try {
|
|
436
|
-
raw = fs.readFileSync(fileArg, 'utf8');
|
|
437
|
-
} catch (e) {
|
|
438
|
-
console.error(`error: could not read --harness-file ${fileArg}: ${e.message}`);
|
|
439
|
-
process.exit(2);
|
|
440
|
-
}
|
|
441
|
-
} else if (jsonArg !== undefined) {
|
|
442
|
-
raw = String(jsonArg);
|
|
443
|
-
} else {
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let parsed;
|
|
448
|
-
try {
|
|
449
|
-
parsed = JSON.parse(raw);
|
|
450
|
-
} catch (e) {
|
|
451
|
-
const src = fileArg !== undefined ? `--harness-file ${fileArg}` : '--harness-json';
|
|
452
|
-
console.error(`error: invalid JSON in ${src}: ${e.message}`);
|
|
453
|
-
process.exit(2);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Resolve the grounding manifest best-effort: explicit id -> completion-report
|
|
457
|
-
// basename. A missing/unresolvable manifest is non-fatal (groundHarnessUsed
|
|
458
|
-
// with null propagated marks everything grounded:false).
|
|
459
|
-
const dispatchId = flags['dispatch-id']
|
|
460
|
-
|| path.basename(process.env.MINIONS_COMPLETION_REPORT || '', '.json')
|
|
461
|
-
|| undefined;
|
|
462
|
-
let propagated = null;
|
|
463
|
-
if (dispatchId) {
|
|
464
|
-
try {
|
|
465
|
-
const dispatch = getDispatch();
|
|
466
|
-
for (const queue of ['active', 'completed', 'pending']) {
|
|
467
|
-
const list = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
468
|
-
if (!list) continue;
|
|
469
|
-
const found = list.find(d => d && d.id === dispatchId);
|
|
470
|
-
if (found && found._harnessPropagated) { propagated = found._harnessPropagated; break; }
|
|
471
|
-
}
|
|
472
|
-
} catch { propagated = null; }
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return shared.groundHarnessUsed(parsed, propagated);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// resolveSkillSkippedForComment(flags) — W-mr287sc0000uc214. Turn the
|
|
479
|
-
// `minions pr comment` skip flags into a canonical `{name, reason}` record for
|
|
480
|
-
// the posters' `skillSkipped` param (engine/gh-comment.js / engine/ado-comment.js),
|
|
481
|
-
// which fold it via buildMinionsCommentBody -> buildSkippedSkillSection into a
|
|
482
|
-
// VISIBLE callout in the same comment as the verdict.
|
|
483
|
-
//
|
|
484
|
-
// Source precedence mirrors the harness resolver: `--skill-skipped-file <path>`
|
|
485
|
-
// (JSON on disk) wins over the inline `--skill-skipped-json <json>`; neither
|
|
486
|
-
// supplied -> returns undefined so the comment body is byte-identical to today
|
|
487
|
-
// (no callout). Read/parse failures are HARD errors (process.exit(2)). Accepts
|
|
488
|
-
// the raw `{name, reason}` shape or a `meta.skill` / `meta.review` wrapper; a
|
|
489
|
-
// record with no non-empty `name` yields undefined (nothing to surface).
|
|
490
|
-
function resolveSkillSkippedForComment(flags) {
|
|
491
|
-
const fileArg = flags['skill-skipped-file'];
|
|
492
|
-
const jsonArg = flags['skill-skipped-json'];
|
|
493
|
-
|
|
494
|
-
let raw;
|
|
495
|
-
if (fileArg !== undefined) {
|
|
496
|
-
try {
|
|
497
|
-
raw = fs.readFileSync(fileArg, 'utf8');
|
|
498
|
-
} catch (e) {
|
|
499
|
-
console.error(`error: could not read --skill-skipped-file ${fileArg}: ${e.message}`);
|
|
500
|
-
process.exit(2);
|
|
501
|
-
}
|
|
502
|
-
} else if (jsonArg !== undefined) {
|
|
503
|
-
raw = String(jsonArg);
|
|
504
|
-
} else {
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
let parsed;
|
|
509
|
-
try {
|
|
510
|
-
parsed = JSON.parse(raw);
|
|
511
|
-
} catch (e) {
|
|
512
|
-
const src = fileArg !== undefined ? `--skill-skipped-file ${fileArg}` : '--skill-skipped-json';
|
|
513
|
-
console.error(`error: invalid JSON in ${src}: ${e.message}`);
|
|
514
|
-
process.exit(2);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const { _extractSkillSkipped } = require('./comment-format');
|
|
518
|
-
return _extractSkillSkipped(parsed) || undefined;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
416
|
const commands = {
|
|
522
417
|
start(...startArgs) {
|
|
523
418
|
// Apply --cli / --model fleet flags before any engine wiring touches
|
|
@@ -539,18 +434,6 @@ const commands = {
|
|
|
539
434
|
throw err;
|
|
540
435
|
}
|
|
541
436
|
|
|
542
|
-
// Startup state-file size guard (#1167): dispatch.json / cooldowns.json
|
|
543
|
-
// bloated past 100 MB silently OOMed V8 on JSON.parse at startup. Fail fast
|
|
544
|
-
// with an actionable message instead.
|
|
545
|
-
try {
|
|
546
|
-
for (const fp of [DISPATCH_PATH, path.join(ENGINE_DIR, 'cooldowns.json')]) {
|
|
547
|
-
shared.assertStateFileSize(fp);
|
|
548
|
-
}
|
|
549
|
-
} catch (stateErr) {
|
|
550
|
-
console.error('\n[engine] STARTUP ABORTED — ' + stateErr.message + '\n');
|
|
551
|
-
process.exit(78); // 78 = configuration error
|
|
552
|
-
}
|
|
553
|
-
|
|
554
437
|
// Run preflight checks (warn but don't block — engine may still be useful)
|
|
555
438
|
try {
|
|
556
439
|
const { runPreflight, printPreflight } = require('./preflight');
|
|
@@ -667,10 +550,7 @@ const commands = {
|
|
|
667
550
|
}
|
|
668
551
|
} catch (err) { e.log('warn', `note-link backfill failed: ${err.message}`); }
|
|
669
552
|
|
|
670
|
-
// Backfill work_items.prd_item_id
|
|
671
|
-
// the dual-write stamp (#546) or that a JSON re-hydrate transiently zeroed.
|
|
672
|
-
// Set-based, only touches NULL FKs, no-op once everything is linked — safe on
|
|
673
|
-
// every boot. Lets the Phase-10 render join move off the feature-id string.
|
|
553
|
+
// Backfill work_items.prd_item_id for rows created before FK stamping.
|
|
674
554
|
try {
|
|
675
555
|
const fk = require('./prd-store').backfillWorkItemPrdItemIds();
|
|
676
556
|
const filled = (fk.byWorkItemId || 0) + (fk.bySourcePlan || 0);
|
|
@@ -1211,22 +1091,7 @@ const commands = {
|
|
|
1211
1091
|
const _watchedFiles = new Set();
|
|
1212
1092
|
let _globalDebounce = null;
|
|
1213
1093
|
function watchForWorkChanges() {
|
|
1214
|
-
const filesToWatch = [
|
|
1215
|
-
path.join(MINIONS_DIR, 'work-items.json'),
|
|
1216
|
-
// dispatch.json excluded — it changes every tick, causing a feedback loop
|
|
1217
|
-
];
|
|
1218
|
-
// Watch project-specific work-items.json
|
|
1219
|
-
const { getProjects } = require('./shared');
|
|
1220
|
-
for (const p of getProjects(config)) {
|
|
1221
|
-
filesToWatch.push(shared.projectWorkItemsPath(p));
|
|
1222
|
-
}
|
|
1223
|
-
// Watch PRD files
|
|
1224
|
-
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
1225
|
-
try {
|
|
1226
|
-
for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
|
|
1227
|
-
filesToWatch.push(path.join(prdDir, f));
|
|
1228
|
-
}
|
|
1229
|
-
} catch { /* optional */ }
|
|
1094
|
+
const filesToWatch = [];
|
|
1230
1095
|
|
|
1231
1096
|
for (const filePath of filesToWatch) {
|
|
1232
1097
|
if (_watchedFiles.has(filePath)) continue;
|
|
@@ -1465,8 +1330,8 @@ const commands = {
|
|
|
1465
1330
|
}
|
|
1466
1331
|
const flagFields = [
|
|
1467
1332
|
'claudeBareMode', 'claudeFallbackModel',
|
|
1468
|
-
'copilotDisableBuiltinMcps', '
|
|
1469
|
-
'
|
|
1333
|
+
'copilotDisableBuiltinMcps', 'copilotStreamMode', 'copilotReasoningSummaries',
|
|
1334
|
+
'claudePreApproveWorkspaceMcps',
|
|
1470
1335
|
'maxBudgetUsd', 'disableModelDiscovery',
|
|
1471
1336
|
'ccUseWorkerPool',
|
|
1472
1337
|
];
|
|
@@ -1488,14 +1353,13 @@ const commands = {
|
|
|
1488
1353
|
}
|
|
1489
1354
|
|
|
1490
1355
|
console.log('');
|
|
1491
|
-
const metrics =
|
|
1356
|
+
const metrics = queries.getMetrics();
|
|
1492
1357
|
const lifetimeCompleted = Object.entries(metrics).filter(([k]) => !k.startsWith('_')).reduce((sum, [, m]) => sum + (m.tasksCompleted || 0) + (m.tasksErrored || 0), 0);
|
|
1493
1358
|
console.log(`Dispatch: ${dispatch.pending.length} pending | ${(dispatch.active || []).length} active | ${lifetimeCompleted} completed`);
|
|
1494
1359
|
const pidSummary = summarizeActiveDispatchPids(dispatch.active || []);
|
|
1495
1360
|
console.log(`Active dispatch PID files: ${pidSummary.live} live | ${pidSummary.dead} dead | ${pidSummary.missing} missing`);
|
|
1496
1361
|
|
|
1497
|
-
const
|
|
1498
|
-
const metricsData = safeJson(metricsPath);
|
|
1362
|
+
const metricsData = queries.getMetrics();
|
|
1499
1363
|
if (metricsData && Object.keys(metricsData).length > 0) {
|
|
1500
1364
|
console.log('\nMetrics:');
|
|
1501
1365
|
console.log(` ${'Agent'.padEnd(12)} ${'Done'.padEnd(6)} ${'Err'.padEnd(6)} ${'PRs'.padEnd(6)} ${'Appr'.padEnd(6)} ${'Rej'.padEnd(6)} ${'Reviews'.padEnd(8)} ${'Cost'.padEnd(8)}`);
|
|
@@ -1989,7 +1853,7 @@ const commands = {
|
|
|
1989
1853
|
doctor(...doctorArgs) {
|
|
1990
1854
|
const { doctor, runHarnessDoctor } = require('./preflight');
|
|
1991
1855
|
if (doctorArgs.includes('--harness')) {
|
|
1992
|
-
//
|
|
1856
|
+
// Read-only runtime harness inventory. See
|
|
1993
1857
|
// docs/harness-propagation.md and the parallel branch in bin/minions.js.
|
|
1994
1858
|
const ok = runHarnessDoctor(MINIONS_DIR);
|
|
1995
1859
|
if (!ok) process.exit(1);
|
|
@@ -2086,19 +1950,9 @@ const commands = {
|
|
|
2086
1950
|
console.log('Usage:');
|
|
2087
1951
|
console.log(' GitHub: minions pr comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2088
1952
|
console.log(' ADO: minions pr comment <prNumber> --host ado --ado-org <org> --ado-project <proj> --repo-id <id> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2089
|
-
console.log('');
|
|
2090
|
-
console.log('Optional harness self-report (folded into the collapsible "Harnesses used" section):');
|
|
2091
|
-
console.log(' --harness-file <path> JSON file with the { skills, mcpServers, commands, docs } record (wins over --harness-json)');
|
|
2092
|
-
console.log(' --harness-json <json> same record inline as a JSON string');
|
|
2093
|
-
console.log(' --dispatch-id <id> grounding manifest override (else basename of $MINIONS_COMPLETION_REPORT)');
|
|
2094
|
-
console.log('');
|
|
2095
|
-
console.log('Skipped in-scope project skill (folded in as a VISIBLE "⚠️ Skipped project skill" callout):');
|
|
2096
|
-
console.log(' --skill-skipped-file <path> JSON file with the { name, reason } skip record (wins over --skill-skipped-json)');
|
|
2097
|
-
console.log(' --skill-skipped-json <json> same record inline as a JSON string (also accepts a meta.skill/meta.review wrapper)');
|
|
2098
1953
|
console.log(' --resolved (ADO only) post the thread pre-resolved/closed — for non-actionable FYI/praise notes');
|
|
2099
1954
|
console.log('');
|
|
2100
|
-
console.log('Posts a PR comment with the hidden minions marker
|
|
2101
|
-
console.log('"Harnesses used" section, and the brand link folded in by the shared');
|
|
1955
|
+
console.log('Posts a PR comment with the hidden minions marker and brand link folded in by the shared');
|
|
2102
1956
|
console.log('builder — so engine classifiers identify agent-authored comments by');
|
|
2103
1957
|
console.log('structure, and the body is byte-identical on GitHub (`gh pr comment`)');
|
|
2104
1958
|
console.log('and Azure DevOps (REST `…/threads`).');
|
|
@@ -2162,18 +2016,6 @@ const commands = {
|
|
|
2162
2016
|
process.exit(2);
|
|
2163
2017
|
}
|
|
2164
2018
|
|
|
2165
|
-
// P-7a3c9e21 — thread the consulted-sources footprint into both posters so
|
|
2166
|
-
// the auto-folded "Harnesses used" <details> section the help already
|
|
2167
|
-
// promises is actually rendered. Resolved once (host-neutral); absent flags
|
|
2168
|
-
// -> undefined -> body byte-identical to today.
|
|
2169
|
-
const harnessUsed = resolveHarnessUsedForComment(flags);
|
|
2170
|
-
|
|
2171
|
-
// W-mr287sc0000uc214 — thread the skipped-in-scope-project-skill self-report
|
|
2172
|
-
// into both posters so it's surfaced as a VISIBLE callout in the same comment
|
|
2173
|
-
// as the verdict (not buried in the completion-report JSON). Absent flags
|
|
2174
|
-
// -> undefined -> body byte-identical to today.
|
|
2175
|
-
const skillSkipped = resolveSkillSkippedForComment(flags);
|
|
2176
|
-
|
|
2177
2019
|
// ── Azure DevOps: <prNumber> --host ado --ado-org --ado-project --repo-id ──
|
|
2178
2020
|
if (isAdo) {
|
|
2179
2021
|
const [prNumberRaw] = positional;
|
|
@@ -2193,7 +2035,7 @@ const commands = {
|
|
|
2193
2035
|
const orgBase = flags['org-base'] || `https://dev.azure.com/${adoOrg}`;
|
|
2194
2036
|
const adoComment = require('./ado-comment');
|
|
2195
2037
|
adoComment.postAdoPrComment({
|
|
2196
|
-
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
|
|
2038
|
+
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
|
|
2197
2039
|
resolved: flags.resolved === true,
|
|
2198
2040
|
}).then((result) => {
|
|
2199
2041
|
if (result && result.threadId) {
|
|
@@ -2232,7 +2074,7 @@ const commands = {
|
|
|
2232
2074
|
|
|
2233
2075
|
try {
|
|
2234
2076
|
const result = ghComment.postPrComment({
|
|
2235
|
-
repo, prNumber, body, agentId, kind, workItemId,
|
|
2077
|
+
repo, prNumber, body, agentId, kind, workItemId,
|
|
2236
2078
|
});
|
|
2237
2079
|
if (result.output) console.log(result.output);
|
|
2238
2080
|
} catch (e) {
|
|
@@ -2371,10 +2213,6 @@ module.exports = {
|
|
|
2371
2213
|
_readDispatchPid: readDispatchPid,
|
|
2372
2214
|
_normalizeSessionBranch: normalizeSessionBranch,
|
|
2373
2215
|
_dispatchSessionBranch: dispatchSessionBranch,
|
|
2374
|
-
// P-7a3c9e21 — harness-flag resolver exported for CLI-threading tests.
|
|
2375
|
-
_resolveHarnessUsedForComment: resolveHarnessUsedForComment,
|
|
2376
|
-
// W-mr287sc0000uc214 — skipped-project-skill flag resolver exported for tests.
|
|
2377
|
-
_resolveSkillSkippedForComment: resolveSkillSkippedForComment,
|
|
2378
2216
|
// W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
|
|
2379
2217
|
_writeHeartbeatNow: writeHeartbeatNow,
|
|
2380
2218
|
_createHeartbeatInterval: createHeartbeatInterval,
|
package/engine/comment-format.js
CHANGED
|
@@ -1,48 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/comment-format.js — platform-neutral PR-comment body fragments.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* dashboard renderer and the consolidation path respectively. This module owns
|
|
7
|
-
* the *PR-comment* surface.
|
|
8
|
-
*
|
|
9
|
-
* `buildHarnessUsedSection(harnessUsed)` renders the grounded harness-usage
|
|
10
|
-
* record (the canonical `{ skills, mcpServers, commands, docs }` shape produced
|
|
11
|
-
* by `shared.groundHarnessUsed` / `shared.normalizeHarnessUsed`; each entry may
|
|
12
|
-
* carry `grounded: true | false`) as a collapsible Markdown `<details>` block.
|
|
13
|
-
*
|
|
14
|
-
* It is deliberately platform-NEUTRAL: the output is plain Markdown with no
|
|
15
|
-
* GitHub- or ADO-specific syntax, so the GitHub comment path
|
|
16
|
-
* (`engine/gh-comment.js buildMinionsCommentBody`) and the ADO comment path
|
|
17
|
-
* (agent-authored via `az repos pr comment` per `playbooks/shared-rules.md`)
|
|
18
|
-
* fold in a **byte-identical** section. Both consume this one helper — there is
|
|
19
|
-
* no second renderer to drift.
|
|
20
|
-
*
|
|
21
|
-
* Returns '' when the record is absent, malformed, or has no renderable entries
|
|
22
|
-
* so callers can append it unconditionally without emitting an empty section.
|
|
4
|
+
* Owns the platform-neutral Minions marker and brand-link formatting shared by
|
|
5
|
+
* GitHub and Azure DevOps comment transports.
|
|
23
6
|
*/
|
|
24
7
|
|
|
25
|
-
// Kind metadata mirrors the dashboard work-item modal (P-d5a6f7c4,
|
|
26
|
-
// dashboard/js/render-work-items.js): same four kinds, same icons, same
|
|
27
|
-
// primary/secondary field mapping, so the PR-comment and modal readouts stay
|
|
28
|
-
// visually consistent.
|
|
29
|
-
const HARNESS_KINDS = [
|
|
30
|
-
{ key: 'skills', icon: '\u{1F9E9}', primary: 'name', secondary: 'source' }, // 🧩
|
|
31
|
-
{ key: 'mcpServers', icon: '\u{1F50C}', primary: 'name', secondary: 'scope' }, // 🔌
|
|
32
|
-
{ key: 'commands', icon: '\u{2318}', primary: 'name', secondary: 'scope' }, // ⌘
|
|
33
|
-
{ key: 'docs', icon: '\u{1F4C4}', primary: 'path', secondary: 'why' }, // 📄
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const HARNESS_SUMMARY_ICON = '\u{1F9F0}'; // 🧰
|
|
37
|
-
const HARNESS_WARN_ICON = '⚠️'; // ⚠️
|
|
38
|
-
|
|
39
|
-
// W-mr287sc0000uc214 — the visible "skipped project skill" callout. The label is
|
|
40
|
-
// a stable literal so buildMinionsCommentBody can dedupe (never stack two
|
|
41
|
-
// callouts) and tests can assert on it. Clamps mirror the harness field caps.
|
|
42
|
-
const SKILL_SKIPPED_LABEL = '**Skipped project skill:**';
|
|
43
|
-
const SKILL_SKIPPED_MAX_NAME_LEN = 200;
|
|
44
|
-
const SKILL_SKIPPED_MAX_REASON_LEN = 1000;
|
|
45
|
-
|
|
46
8
|
// Canonical Minions site URL. shared-rules.md ("Brand link (MANDATORY)") requires
|
|
47
9
|
// the human-readable "Minions" mention in a PR/ADO comment to render as a markdown
|
|
48
10
|
// hyperlink to this destination. Keep this in sync with the literal in
|
|
@@ -76,118 +38,10 @@ function linkifyBrandTrailer(text) {
|
|
|
76
38
|
return text.replace(BRAND_TRAILER_RE, `by [Minions](${MINIONS_BRAND_URL})`);
|
|
77
39
|
}
|
|
78
40
|
|
|
79
|
-
// Collapse internal whitespace/newlines to single spaces and neutralize
|
|
80
|
-
// backticks so each entry renders on one line and can't break the inline-code
|
|
81
|
-
// span. Values arrive already trimmed + length-clamped by normalizeHarnessUsed,
|
|
82
|
-
// but may still contain internal newlines.
|
|
83
|
-
function _clean(value) {
|
|
84
|
-
return String(value).replace(/`/g, "'").replace(/\s+/g, ' ').trim();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Render the grounded harnessUsed record as a collapsible Markdown section.
|
|
89
|
-
* @param {object|null} harnessUsed canonical { skills, mcpServers, commands, docs }
|
|
90
|
-
* @returns {string} '' when empty, else a `<details>` block (no trailing newline)
|
|
91
|
-
*/
|
|
92
|
-
function buildHarnessUsedSection(harnessUsed) {
|
|
93
|
-
if (!harnessUsed || typeof harnessUsed !== 'object' || Array.isArray(harnessUsed)) return '';
|
|
94
|
-
|
|
95
|
-
const lines = [];
|
|
96
|
-
let anyUngrounded = false;
|
|
97
|
-
|
|
98
|
-
for (const kind of HARNESS_KINDS) {
|
|
99
|
-
const entries = Array.isArray(harnessUsed[kind.key]) ? harnessUsed[kind.key] : [];
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
102
|
-
const primaryRaw = entry[kind.primary];
|
|
103
|
-
if (typeof primaryRaw !== 'string' || !primaryRaw.trim()) continue;
|
|
104
|
-
const primary = _clean(primaryRaw);
|
|
105
|
-
|
|
106
|
-
const grounded = entry.grounded === true;
|
|
107
|
-
if (!grounded) anyUngrounded = true;
|
|
108
|
-
|
|
109
|
-
const secondaryRaw = entry[kind.secondary];
|
|
110
|
-
const secondary = (typeof secondaryRaw === 'string' && secondaryRaw.trim())
|
|
111
|
-
? _clean(secondaryRaw) : '';
|
|
112
|
-
|
|
113
|
-
const tail = secondary ? ` — ${secondary}` : ''; // em dash
|
|
114
|
-
const warn = grounded ? '' : ` ${HARNESS_WARN_ICON}`;
|
|
115
|
-
lines.push(`- ${kind.icon} \`${primary}\`${tail}${warn}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (lines.length === 0) return '';
|
|
120
|
-
|
|
121
|
-
const legend = anyUngrounded
|
|
122
|
-
? `\n\n> ${HARNESS_WARN_ICON} agent-reported, not verified by the engine`
|
|
123
|
-
: '';
|
|
124
|
-
|
|
125
|
-
return `<details>\n<summary>${HARNESS_SUMMARY_ICON} Harnesses used (${lines.length})</summary>\n\n`
|
|
126
|
-
+ `${lines.join('\n')}${legend}\n\n</details>`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extract a canonical `{ name, reason }` skip record from a self-reported skip
|
|
131
|
-
* value. Accepts the raw `{ name, reason }` shape directly, or a `meta` wrapper
|
|
132
|
-
* that carries it — `meta.skill` (`{ skipped: { name, reason } }`) or
|
|
133
|
-
* `meta.review` (`{ skillSkipped: { name, reason } }`) — so callers can pass the
|
|
134
|
-
* completion-report block verbatim. Returns null when no non-empty `name` is
|
|
135
|
-
* present so callers treat "no skip" as a single falsy case.
|
|
136
|
-
* @param {*} input
|
|
137
|
-
* @returns {{name:string, reason:string}|null}
|
|
138
|
-
*/
|
|
139
|
-
function _extractSkillSkipped(input) {
|
|
140
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
141
|
-
let node = input;
|
|
142
|
-
if (input.skipped && typeof input.skipped === 'object' && !Array.isArray(input.skipped)) {
|
|
143
|
-
node = input.skipped; // meta.skill shape
|
|
144
|
-
} else if (input.skillSkipped && typeof input.skillSkipped === 'object' && !Array.isArray(input.skillSkipped)) {
|
|
145
|
-
node = input.skillSkipped; // meta.review shape
|
|
146
|
-
}
|
|
147
|
-
const name = typeof node.name === 'string' ? node.name.trim() : '';
|
|
148
|
-
if (!name) return null;
|
|
149
|
-
const reason = typeof node.reason === 'string' ? node.reason.trim() : '';
|
|
150
|
-
return { name, reason };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Render the "skipped project skill" self-report as a VISIBLE Markdown
|
|
155
|
-
* blockquote callout (W-mr287sc0000uc214), e.g.:
|
|
156
|
-
* > ⚠️ **Skipped project skill:** `FMF-review-context` — small 2-file diff…
|
|
157
|
-
*
|
|
158
|
-
* A review/fix agent may self-report that it intentionally skipped an available,
|
|
159
|
-
* in-scope project (review) skill via `meta.review.skillSkipped` /
|
|
160
|
-
* `meta.skill.skipped` (shape `{name, reason}`). That skip used to live ONLY in
|
|
161
|
-
* the completion-report JSON — invisible to a human reading the PR before merge.
|
|
162
|
-
* This is a sibling of buildHarnessUsedSection (same neutral chokepoint, same
|
|
163
|
-
* `_clean` hygiene) but a VISIBLE blockquote rather than a collapsed <details>:
|
|
164
|
-
* the whole point is that a human reviewing the PR sees the skip rationale in the
|
|
165
|
-
* same comment as the APPROVE / REQUEST_CHANGES verdict, before merge.
|
|
166
|
-
*
|
|
167
|
-
* Deliberately platform-NEUTRAL Markdown so the GitHub and ADO posters fold in a
|
|
168
|
-
* byte-identical callout — there is no second renderer to drift. Accepts either
|
|
169
|
-
* the raw `{name, reason}` record or a `meta.skill` / `meta.review` wrapper.
|
|
170
|
-
*
|
|
171
|
-
* Returns '' when the record is absent/malformed (no skip name), so callers can
|
|
172
|
-
* append it unconditionally without emitting a stray callout — reports without
|
|
173
|
-
* meta.review.skillSkipped / meta.skill.skipped render identically to today.
|
|
174
|
-
*
|
|
175
|
-
* @param {object|null} skillSkipped `{name, reason}` or a meta.skill/meta.review wrapper
|
|
176
|
-
* @returns {string} '' when empty, else a one-line `> ⚠️ …` blockquote
|
|
177
|
-
*/
|
|
178
|
-
function buildSkippedSkillSection(skillSkipped) {
|
|
179
|
-
const parsed = _extractSkillSkipped(skillSkipped);
|
|
180
|
-
if (!parsed) return '';
|
|
181
|
-
const name = _clean(parsed.name).slice(0, SKILL_SKIPPED_MAX_NAME_LEN);
|
|
182
|
-
const reasonClean = parsed.reason ? _clean(parsed.reason).slice(0, SKILL_SKIPPED_MAX_REASON_LEN) : '';
|
|
183
|
-
const tail = reasonClean ? ` — ${reasonClean}` : ''; // em dash
|
|
184
|
-
return `> ${HARNESS_WARN_ICON} ${SKILL_SKIPPED_LABEL} \`${name}\`${tail}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
41
|
// ── Minions marker + neutral comment-body builder ───────────────────────────
|
|
188
42
|
//
|
|
189
43
|
// The hidden HTML-comment marker and the body composition (marker + harness
|
|
190
|
-
//
|
|
44
|
+
// brand link) are platform-NEUTRAL: GitHub (engine/gh-comment.js) and
|
|
191
45
|
// Azure DevOps (engine/ado-comment.js) both post the byte-identical body built
|
|
192
46
|
// here, so there is one builder and no drift. The host modules own only the
|
|
193
47
|
// transport (gh CLI vs ADO REST) and host-specific validation (repo slug vs
|
|
@@ -246,42 +100,21 @@ function _buildMarker({ agentId, kind, workItemId }) {
|
|
|
246
100
|
}
|
|
247
101
|
|
|
248
102
|
/**
|
|
249
|
-
* Compose a minions PR-comment body: prepend the hidden marker
|
|
250
|
-
*
|
|
251
|
-
* trailer. Platform-neutral — GitHub and ADO posters both call this.
|
|
103
|
+
* Compose a minions PR-comment body: prepend the hidden marker and hyperlink the
|
|
104
|
+
* first bare brand trailer. Platform-neutral — GitHub and ADO posters both call this.
|
|
252
105
|
*
|
|
253
106
|
* Idempotency: if `body` already starts with a minions marker it is returned
|
|
254
|
-
* with the
|
|
107
|
+
* with the brand transform applied but no second marker prepended.
|
|
255
108
|
*
|
|
256
|
-
* @param {{agentId:string, kind:string, workItemId?:string, body?:string
|
|
109
|
+
* @param {{agentId:string, kind:string, workItemId?:string, body?:string}} args
|
|
257
110
|
* @returns {string} the final comment body
|
|
258
111
|
*/
|
|
259
|
-
function buildMinionsCommentBody({ agentId, kind, workItemId, body
|
|
112
|
+
function buildMinionsCommentBody({ agentId, kind, workItemId, body }) {
|
|
260
113
|
// Validate the inputs even when the body is pre-marked, so callers can't
|
|
261
114
|
// silently bypass validation by pre-marking their body.
|
|
262
115
|
_validateMarkerInputs({ agentId, kind, workItemId });
|
|
263
116
|
let safeBody = body == null ? '' : String(body);
|
|
264
117
|
|
|
265
|
-
// Fold in the VISIBLE "skipped project skill" callout (W-mr287sc0000uc214)
|
|
266
|
-
// BEFORE the collapsed harness section so a human sees the skip rationale in
|
|
267
|
-
// the same comment as the verdict — not buried in a <details> or reachable
|
|
268
|
-
// only via a separate API call. Dedup on the stable label so a re-marked body
|
|
269
|
-
// already carrying the callout doesn't stack a second one. Absent/malformed
|
|
270
|
-
// skip → '' → no-op (byte-identical to today).
|
|
271
|
-
const skipSection = buildSkippedSkillSection(skillSkipped);
|
|
272
|
-
if (skipSection && !safeBody.includes(SKILL_SKIPPED_LABEL)) {
|
|
273
|
-
safeBody = safeBody ? `${safeBody}\n\n${skipSection}` : skipSection;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Fold the grounded harness-usage digest in as a collapsible <details>
|
|
277
|
-
// section. Append at the END so it survives the marker-idempotency path;
|
|
278
|
-
// dedup on the summary line so a re-marked body that already carries the
|
|
279
|
-
// section doesn't stack a second one.
|
|
280
|
-
const harnessSection = buildHarnessUsedSection(harnessUsed);
|
|
281
|
-
if (harnessSection && !safeBody.includes('<summary>\u{1F9F0} Harnesses used')) {
|
|
282
|
-
safeBody = safeBody ? `${safeBody}\n\n${harnessSection}` : harnessSection;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
118
|
// Brand-link safety net: deterministically hyperlink the first bare
|
|
286
119
|
// "… by Minions" signature trailer (no-op when the agent already linked it).
|
|
287
120
|
safeBody = linkifyBrandTrailer(safeBody);
|
|
@@ -307,8 +140,6 @@ function parseMinionsMarker(body) {
|
|
|
307
140
|
}
|
|
308
141
|
|
|
309
142
|
module.exports = {
|
|
310
|
-
buildHarnessUsedSection,
|
|
311
|
-
buildSkippedSkillSection,
|
|
312
143
|
linkifyBrandTrailer,
|
|
313
144
|
MINIONS_BRAND_URL,
|
|
314
145
|
// Neutral marker + body builder (consumed by gh-comment.js and ado-comment.js).
|
|
@@ -322,9 +153,4 @@ module.exports = {
|
|
|
322
153
|
// Internal helpers exported for tests / host posters.
|
|
323
154
|
_buildMarker,
|
|
324
155
|
_validateMarkerInputs,
|
|
325
|
-
// Exported for tests / advanced callers that want to mirror the kind mapping.
|
|
326
|
-
HARNESS_KINDS,
|
|
327
|
-
// W-mr287sc0000uc214 — skipped-project-skill callout helpers.
|
|
328
|
-
_extractSkillSkipped,
|
|
329
|
-
SKILL_SKIPPED_LABEL,
|
|
330
156
|
};
|
package/engine/db/index.js
CHANGED
|
@@ -21,6 +21,45 @@ let _db = null;
|
|
|
21
21
|
let _dbPath = null;
|
|
22
22
|
let _dbInitError = null;
|
|
23
23
|
let _exitCheckpointRegistered = false;
|
|
24
|
+
let _cleanedDbPath = null;
|
|
25
|
+
|
|
26
|
+
function _cleanupRetiredJsonFiles(db, dbPath) {
|
|
27
|
+
if (_cleanedDbPath === dbPath) return;
|
|
28
|
+
const marker = db.prepare("SELECT value FROM state_markers WHERE key='sql_only_cutover'").get();
|
|
29
|
+
if (!marker) return;
|
|
30
|
+
const minionsDir = path.dirname(path.dirname(dbPath));
|
|
31
|
+
const engineDir = path.join(minionsDir, 'engine');
|
|
32
|
+
const engineFiles = [
|
|
33
|
+
'dispatch.json', 'log.json', 'metrics.json', 'watches.json',
|
|
34
|
+
'schedule-runs.json', 'pipeline-runs.json', 'managed-processes.json',
|
|
35
|
+
'worktree-pool.json', 'qa-runs.json', 'qa-sessions.json', 'pr-links.json',
|
|
36
|
+
'cooldowns.json', 'pending-rebases.json', 'cc-sessions.json', 'doc-sessions.json',
|
|
37
|
+
];
|
|
38
|
+
const targets = engineFiles.map(name => path.join(engineDir, name));
|
|
39
|
+
targets.push(path.join(minionsDir, 'work-items.json'), path.join(minionsDir, 'pull-requests.json'));
|
|
40
|
+
try {
|
|
41
|
+
for (const project of fs.readdirSync(path.join(minionsDir, 'projects'), { withFileTypes: true })) {
|
|
42
|
+
if (!project.isDirectory() || project.name === '.archived') continue;
|
|
43
|
+
targets.push(
|
|
44
|
+
path.join(minionsDir, 'projects', project.name, 'work-items.json'),
|
|
45
|
+
path.join(minionsDir, 'projects', project.name, 'pull-requests.json'),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
} catch { /* projects directory is optional */ }
|
|
49
|
+
for (const dir of [path.join(minionsDir, 'prd'), path.join(minionsDir, 'prd', 'archive')]) {
|
|
50
|
+
try {
|
|
51
|
+
for (const name of fs.readdirSync(dir)) {
|
|
52
|
+
if (name.endsWith('.json')) targets.push(path.join(dir, name));
|
|
53
|
+
}
|
|
54
|
+
} catch { /* PRD directory is optional */ }
|
|
55
|
+
}
|
|
56
|
+
for (const target of targets) {
|
|
57
|
+
for (const suffix of ['', '.backup', '.lock']) {
|
|
58
|
+
try { fs.unlinkSync(target + suffix); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_cleanedDbPath = dbPath;
|
|
62
|
+
}
|
|
24
63
|
|
|
25
64
|
// DATA-LOSS GUARD: register a one-time process-exit hook that checkpoints +
|
|
26
65
|
// closes the DB. Registered by getDb() on first successful open so EVERY DB
|
|
@@ -68,14 +107,6 @@ function _installExperimentalWarningFilter() {
|
|
|
68
107
|
}
|
|
69
108
|
|
|
70
109
|
function getDb() {
|
|
71
|
-
// Hard opt-out (issue #3035, option #3): MINIONS_FORCE_JSON=1 makes
|
|
72
|
-
// every getDb() call throw, forcing the entire stack onto the JSON
|
|
73
|
-
// fallback path. Useful for users who can't enable
|
|
74
|
-
// --experimental-sqlite on Node 22.x and for regression tests that
|
|
75
|
-
// need to exercise the JSON-fallback branches deterministically.
|
|
76
|
-
if (process.env.MINIONS_FORCE_JSON === '1') {
|
|
77
|
-
throw new Error('engine/db: SQL disabled via MINIONS_FORCE_JSON=1');
|
|
78
|
-
}
|
|
79
110
|
// Re-resolve the DB path on every call so tests that swap MINIONS_TEST_DIR
|
|
80
111
|
// (or production-side configuration that changes MINIONS_HOME between
|
|
81
112
|
// operations) get a fresh connection to the new location instead of
|
|
@@ -143,12 +174,13 @@ function getDb() {
|
|
|
143
174
|
if (lastErr) throw lastErr;
|
|
144
175
|
const { runMigrations } = require('./migrate');
|
|
145
176
|
runMigrations(_db);
|
|
177
|
+
_cleanupRetiredJsonFiles(_db, dbPath);
|
|
146
178
|
_registerExitCheckpoint();
|
|
147
179
|
return _db;
|
|
148
180
|
} catch (e) {
|
|
149
181
|
const nodeMajor = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
150
182
|
const flagHint = nodeMajor === 22
|
|
151
|
-
? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite
|
|
183
|
+
? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite or upgrade to Node 24+.`
|
|
152
184
|
: ` Node 22.5+ (with --experimental-sqlite on 22.x) or 24+ required for built-in 'node:sqlite' support.`;
|
|
153
185
|
_dbInitError = new Error(`engine/db: failed to open SQLite — ${e.message}.${flagHint}`);
|
|
154
186
|
throw _dbInitError;
|
|
@@ -200,4 +232,22 @@ function withTransaction(db, fn) {
|
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
|
|
203
|
-
|
|
235
|
+
// Cache compiled statements on the db connection itself so hot, fixed-shape
|
|
236
|
+
// read paths (e.g. FTS5 memory search run repeatedly per dispatch) don't pay
|
|
237
|
+
// SQLite's parse/plan cost on every call — `db.prepare()` recompiles from
|
|
238
|
+
// scratch each time it's invoked, node:sqlite does not dedupe by text. Keyed
|
|
239
|
+
// per-connection (not module-level) so closeDb()/getDb() reopening against a
|
|
240
|
+
// new path (test isolation) naturally starts with an empty cache instead of
|
|
241
|
+
// holding stale statements bound to a closed handle.
|
|
242
|
+
function prepareCached(db, sql) {
|
|
243
|
+
if (!db) throw new Error('engine/db.prepareCached: db is required');
|
|
244
|
+
if (!db._minionsStmtCache) db._minionsStmtCache = new Map();
|
|
245
|
+
let stmt = db._minionsStmtCache.get(sql);
|
|
246
|
+
if (!stmt) {
|
|
247
|
+
stmt = db.prepare(sql);
|
|
248
|
+
db._minionsStmtCache.set(sql, stmt);
|
|
249
|
+
}
|
|
250
|
+
return stmt;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { getDb, closeDb, withTransaction, prepareCached };
|