@yemi33/minions 0.1.2370 → 0.1.2372
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/lifecycle.js
CHANGED
|
@@ -16,7 +16,6 @@ const queries = require('./queries');
|
|
|
16
16
|
const harness = require('./harness');
|
|
17
17
|
const { isBranchActive } = require('./cooldown');
|
|
18
18
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
19
|
-
const { buildHarnessUsedSection } = require('./comment-format');
|
|
20
19
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
21
20
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
22
21
|
|
|
@@ -520,17 +519,9 @@ function checkPlanCompletion(meta, config) {
|
|
|
520
519
|
function archivePlan(planFile, plan, projects, config) {
|
|
521
520
|
const planPath = path.join(PRD_DIR, planFile);
|
|
522
521
|
|
|
523
|
-
//
|
|
524
|
-
// where it sits, rather than MOVING it to prd/archive/. This matches the
|
|
525
|
-
// canonical dashboard (_archivePrdPostProcess, #533) + watch-action archive
|
|
526
|
-
// paths. The legacy move was the LAST path still physically relocating a PRD,
|
|
527
|
-
// and it reintroduced the live↔archive basename-collision class (footgun #7)
|
|
528
|
-
// that the in-place flag model retired — plus it needed a .backup neutralize
|
|
529
|
-
// that is moot now that prd/*.json never gets a .backup sidecar at all (#570).
|
|
530
|
-
// mutateJsonFileLocked dual-writes the flag into SQL via the chokepoint; the
|
|
531
|
-
// archived-PRD scanner guards (#530) already treat status:'archived' correctly.
|
|
522
|
+
// Archive in place by flagging the authoritative SQL PRD.
|
|
532
523
|
try {
|
|
533
|
-
if (
|
|
524
|
+
if (safeJsonNoRestore(planPath)) {
|
|
534
525
|
mutateJsonFileLocked(planPath, (data) => {
|
|
535
526
|
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
536
527
|
data.status = 'archived';
|
|
@@ -842,7 +833,9 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
842
833
|
if (!_VALID_PRD_STATUSES.has(status)) return;
|
|
843
834
|
try {
|
|
844
835
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
845
|
-
const files = sourcePlan
|
|
836
|
+
const files = sourcePlan
|
|
837
|
+
? [sourcePlan]
|
|
838
|
+
: require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
846
839
|
for (const pf of files) {
|
|
847
840
|
const fpath = path.join(prdDir, pf);
|
|
848
841
|
// Lock-free peek: most PRDs won't contain the ID, so skip the lock cost.
|
|
@@ -880,7 +873,6 @@ function stampPrdItemWorkItemId(itemId, sourcePlan) {
|
|
|
880
873
|
if (!itemId || !sourcePlan) return;
|
|
881
874
|
try {
|
|
882
875
|
const fpath = path.join(PRD_DIR, sourcePlan);
|
|
883
|
-
if (!fs.existsSync(fpath)) return;
|
|
884
876
|
const plan = safeJsonNoRestore(fpath);
|
|
885
877
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
886
878
|
if (!feature || feature.workItemId === itemId) return;
|
|
@@ -896,7 +888,6 @@ function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
|
|
|
896
888
|
if (!itemId || !sourcePlan || !prUrl) return;
|
|
897
889
|
try {
|
|
898
890
|
const fpath = path.join(PRD_DIR, sourcePlan);
|
|
899
|
-
if (!fs.existsSync(fpath)) return;
|
|
900
891
|
const plan = safeJsonNoRestore(fpath);
|
|
901
892
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
902
893
|
if (!feature || feature.pr_url === prUrl) return;
|
|
@@ -916,9 +907,7 @@ function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
|
|
|
916
907
|
// different ID than the original PRD feature, leaving the PRD status stale.
|
|
917
908
|
|
|
918
909
|
function reconcilePrdStatuses(config) {
|
|
919
|
-
|
|
920
|
-
let prdFiles;
|
|
921
|
-
try { prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
|
|
910
|
+
const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
922
911
|
if (prdFiles.length === 0) return;
|
|
923
912
|
|
|
924
913
|
const allWorkItems = queries.getWorkItems(config);
|
|
@@ -1537,35 +1526,17 @@ function stampWiPrRef(meta, config) {
|
|
|
1537
1526
|
const projects = (config && Array.isArray(config.projects))
|
|
1538
1527
|
? config.projects : shared.getProjects(config || {});
|
|
1539
1528
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const pr = safeJsonArr(projectPrPath(p)).find(
|
|
1544
|
-
r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)
|
|
1545
|
-
);
|
|
1546
|
-
if (pr) { found = pr; break; }
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// 2. Fall back to central pull-requests.json.
|
|
1550
|
-
if (!found) {
|
|
1551
|
-
found = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json')).find(
|
|
1552
|
-
r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1529
|
+
const prStore = require('./pull-requests-store');
|
|
1530
|
+
const allPrs = prStore.readAllPullRequests();
|
|
1531
|
+
let found = allPrs.find(r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)) || null;
|
|
1555
1532
|
|
|
1556
|
-
//
|
|
1533
|
+
// Fall back to pr-links index (catches cases where prdItems wasn't
|
|
1557
1534
|
// populated but a pr-links entry links the PR to this WI).
|
|
1558
1535
|
if (!found) {
|
|
1559
1536
|
const prLinks = getPrLinks();
|
|
1560
1537
|
const linkedPrId = Object.keys(prLinks).find(id => (prLinks[id] || []).includes(itemId));
|
|
1561
1538
|
if (linkedPrId) {
|
|
1562
|
-
|
|
1563
|
-
const pr = safeJsonArr(projectPrPath(p)).find(r => r.id === linkedPrId);
|
|
1564
|
-
if (pr) { found = pr; break; }
|
|
1565
|
-
}
|
|
1566
|
-
if (!found) {
|
|
1567
|
-
found = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json')).find(r => r.id === linkedPrId);
|
|
1568
|
-
}
|
|
1539
|
+
found = allPrs.find(r => r.id === linkedPrId) || null;
|
|
1569
1540
|
// Worst case: only the canonical ID is known — at least stamp _pr.
|
|
1570
1541
|
if (!found) found = { id: linkedPrId };
|
|
1571
1542
|
}
|
|
@@ -1632,21 +1603,15 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
1632
1603
|
|
|
1633
1604
|
function readOptionalJsonStrict(filePath, label, validate) {
|
|
1634
1605
|
if (!filePath) return null;
|
|
1635
|
-
let raw;
|
|
1636
|
-
try {
|
|
1637
|
-
raw = fs.readFileSync(filePath, 'utf8');
|
|
1638
|
-
} catch (err) {
|
|
1639
|
-
if (err.code === 'ENOENT') return null;
|
|
1640
|
-
throw new Error(`Cannot read ${label} JSON at ${filePath}: ${err.message}`);
|
|
1641
|
-
}
|
|
1642
1606
|
let parsed;
|
|
1643
1607
|
try {
|
|
1644
|
-
parsed =
|
|
1608
|
+
parsed = shared.safeJson(filePath);
|
|
1645
1609
|
} catch (err) {
|
|
1646
|
-
throw new Error(`
|
|
1610
|
+
throw new Error(`Cannot read ${label} state at ${filePath}: ${err.message}`);
|
|
1647
1611
|
}
|
|
1612
|
+
if (parsed == null) return null;
|
|
1648
1613
|
if (validate && !validate(parsed)) {
|
|
1649
|
-
throw new Error(`Invalid ${label}
|
|
1614
|
+
throw new Error(`Invalid ${label} state shape at ${filePath}`);
|
|
1650
1615
|
}
|
|
1651
1616
|
return parsed;
|
|
1652
1617
|
}
|
|
@@ -2109,7 +2074,7 @@ function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary)
|
|
|
2109
2074
|
const wiPath = resolveWorkItemPath(meta);
|
|
2110
2075
|
let syncFailedToPrd = false;
|
|
2111
2076
|
if (wiPath) {
|
|
2112
|
-
|
|
2077
|
+
mutateWorkItems(wiPath, data => {
|
|
2113
2078
|
if (!Array.isArray(data)) return data;
|
|
2114
2079
|
const w = data.find(i => i.id === meta.item.id);
|
|
2115
2080
|
if (!w) return data;
|
|
@@ -2253,7 +2218,7 @@ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
|
|
|
2253
2218
|
|
|
2254
2219
|
const wiPath = resolveWorkItemPath(meta);
|
|
2255
2220
|
if (wiPath) {
|
|
2256
|
-
|
|
2221
|
+
mutateWorkItems(wiPath, data => {
|
|
2257
2222
|
if (!Array.isArray(data)) return data;
|
|
2258
2223
|
const w = data.find(i => i.id === item.id);
|
|
2259
2224
|
if (!w) return data;
|
|
@@ -3701,7 +3666,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
3701
3666
|
// Mark PRD feature as implemented
|
|
3702
3667
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
3703
3668
|
try {
|
|
3704
|
-
const planFiles =
|
|
3669
|
+
const planFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
3705
3670
|
let updated = 0;
|
|
3706
3671
|
for (const pf of planFiles) {
|
|
3707
3672
|
const planPath = path.join(prdDir, pf);
|
|
@@ -4247,28 +4212,6 @@ function handleInjectionFlag(dispatchItem, agentId, structuredCompletion, config
|
|
|
4247
4212
|
return { description, sources, at };
|
|
4248
4213
|
}
|
|
4249
4214
|
|
|
4250
|
-
// resolveHarnessPropagated(dispatchItem) — P-c6f5e8b3. Locate the spawn-time
|
|
4251
|
-
// `_harnessPropagated` manifest for a dispatch. The in-memory dispatchItem
|
|
4252
|
-
// (spawnAgent's closure object) normally carries it directly; after an engine
|
|
4253
|
-
// restart + re-attach the field may only live on the persisted dispatch record,
|
|
4254
|
-
// so fall back to scanning the live dispatch queues by id. Returns null when no
|
|
4255
|
-
// manifest was captured (grounding then marks every entry grounded:false — the
|
|
4256
|
-
// honest "no record" state).
|
|
4257
|
-
function resolveHarnessPropagated(dispatchItem) {
|
|
4258
|
-
if (!dispatchItem) return null;
|
|
4259
|
-
if (dispatchItem._harnessPropagated) return dispatchItem._harnessPropagated;
|
|
4260
|
-
try {
|
|
4261
|
-
const dispatch = getDispatch();
|
|
4262
|
-
for (const queue of ['active', 'completed', 'pending']) {
|
|
4263
|
-
const list = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
4264
|
-
if (!list) continue;
|
|
4265
|
-
const found = list.find(d => d && d.id === dispatchItem.id);
|
|
4266
|
-
if (found && found._harnessPropagated) return found._harnessPropagated;
|
|
4267
|
-
}
|
|
4268
|
-
} catch { /* best-effort */ }
|
|
4269
|
-
return null;
|
|
4270
|
-
}
|
|
4271
|
-
|
|
4272
4215
|
function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
4273
4216
|
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem?.id);
|
|
4274
4217
|
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
@@ -4434,19 +4377,7 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
|
|
|
4434
4377
|
const outputLog = opts.outputLog ? String(opts.outputLog) : '';
|
|
4435
4378
|
const branch = opts.branch || meta.branch || '';
|
|
4436
4379
|
const resultSummary = opts.resultSummary ? String(opts.resultSummary).slice(0, 500) : '';
|
|
4437
|
-
|
|
4438
|
-
// harnessUsed object (canonical { skills, mcpServers, commands, docs } shape
|
|
4439
|
-
// with a strict-boolean `grounded` on each entry, produced upstream in
|
|
4440
|
-
// runPostCompletionHooks via shared.groundHarnessUsed) rides here on
|
|
4441
|
-
// structuredCompletion.harnessUsed. Persist it onto the work item as
|
|
4442
|
-
// `_harnessUsed` so the dashboard work-item detail modal can render the
|
|
4443
|
-
// "Repo harnesses used" section. `null`/absent when the agent reported no
|
|
4444
|
-
// harness usage; only a plain-object survives normalization upstream.
|
|
4445
|
-
const harnessUsed = shared.isPlainObject(structuredCompletion?.harnessUsed)
|
|
4446
|
-
? structuredCompletion.harnessUsed
|
|
4447
|
-
: null;
|
|
4448
|
-
|
|
4449
|
-
if (artifacts.length === 0 && notes.length === 0 && !outputLog && !branch && !resultSummary && !harnessUsed) {
|
|
4380
|
+
if (artifacts.length === 0 && notes.length === 0 && !outputLog && !branch && !resultSummary) {
|
|
4450
4381
|
return { artifacts, notes };
|
|
4451
4382
|
}
|
|
4452
4383
|
|
|
@@ -4465,7 +4396,6 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
|
|
|
4465
4396
|
if (meta.item?.sourcePlan) arts.sourcePlan = meta.item.sourcePlan;
|
|
4466
4397
|
if (artifacts.length > 0) wi.artifacts = mergeCompletionArtifacts(wi.artifacts, artifacts);
|
|
4467
4398
|
if (resultSummary) wi.resultSummary = resultSummary;
|
|
4468
|
-
if (harnessUsed) wi._harnessUsed = harnessUsed;
|
|
4469
4399
|
wi._artifacts = arts;
|
|
4470
4400
|
return data;
|
|
4471
4401
|
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
@@ -4798,11 +4728,6 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
|
|
|
4798
4728
|
const structuredLines = structuredCompletion
|
|
4799
4729
|
? Object.entries(structuredCompletion).map(([key, value]) => `- ${key}: ${value}`).join('\n')
|
|
4800
4730
|
: '- none';
|
|
4801
|
-
// #308 — fold the agent's grounded harness footprint into THIS final agent
|
|
4802
|
-
// report instead of emitting a separate `harness-usage-*` inbox note per
|
|
4803
|
-
// attempt. buildHarnessUsedSection returns '' for an absent/empty/malformed
|
|
4804
|
-
// record, so the section is appended only when there is something to show.
|
|
4805
|
-
const harnessSection = buildHarnessUsedSection(structuredCompletion?.harnessUsed);
|
|
4806
4731
|
const content = [
|
|
4807
4732
|
`# Agent ${outcome === 'partial' ? 'Partially Completed' : 'Reported Failure'}: ${title}`,
|
|
4808
4733
|
'',
|
|
@@ -4817,7 +4742,6 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
|
|
|
4817
4742
|
structuredLines,
|
|
4818
4743
|
'',
|
|
4819
4744
|
resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
|
|
4820
|
-
harnessSection ? `\n${harnessSection}` : '',
|
|
4821
4745
|
].filter(Boolean).join('\n');
|
|
4822
4746
|
shared.writeToInbox(agentId || 'engine', `agent-${outcome}-${dispatchItem.id}`, content, null, metadata);
|
|
4823
4747
|
}
|
|
@@ -5559,31 +5483,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5559
5483
|
structuredCompletion.status = 'failed-injection-flagged';
|
|
5560
5484
|
}
|
|
5561
5485
|
}
|
|
5562
|
-
// P-c6f5e8b3 — harness grounding cross-check (Stage 2). Cross-check the
|
|
5563
|
-
// agent's self-reported `harnessUsed` against the spawn-time
|
|
5564
|
-
// `_harnessPropagated` manifest and annotate each entry `grounded:true|false`.
|
|
5565
|
-
// The grounded result replaces `structuredCompletion.harnessUsed` IN PLACE, so
|
|
5566
|
-
// it rides the existing `structuredCompletion` storage onto the completed
|
|
5567
|
-
// dispatch record (completeDispatch persists `item.structuredCompletion`) for
|
|
5568
|
-
// the Stage-3 surfaces (PR comment / final agent note / WI modal) to read.
|
|
5569
|
-
// Pure, non-destructive (grounded:false entries are kept + flagged, never
|
|
5570
|
-
// dropped), and best-effort: any failure here must never block completion.
|
|
5571
|
-
//
|
|
5572
|
-
// #308 — the grounded footprint is no longer routed to its own
|
|
5573
|
-
// `harness-usage-*` inbox note (that produced one duplicate-looking note chip
|
|
5574
|
-
// per attempt). For clean completions the structured `_harnessUsed` WI-modal
|
|
5575
|
-
// UI surfaces it; for non-clean outcomes writeNonCleanAgentReport folds the
|
|
5576
|
-
// same buildHarnessUsedSection block into the single final agent note.
|
|
5577
|
-
if (structuredCompletion && structuredCompletion.harnessUsed) {
|
|
5578
|
-
try {
|
|
5579
|
-
const propagated = resolveHarnessPropagated(dispatchItem);
|
|
5580
|
-
const grounded = shared.groundHarnessUsed(structuredCompletion.harnessUsed, propagated);
|
|
5581
|
-
structuredCompletion.harnessUsed = grounded; // canonical grounded shape, or null when nothing survived
|
|
5582
|
-
} catch (err) {
|
|
5583
|
-
log('warn', `Harness grounding cross-check failed for ${dispatchItem.id} (non-fatal): ${err.message}`);
|
|
5584
|
-
}
|
|
5585
|
-
}
|
|
5586
|
-
|
|
5587
5486
|
const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
|
|
5588
5487
|
|
|
5589
5488
|
// Save session for potential resume on next dispatch
|
|
@@ -5763,59 +5662,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5763
5662
|
}
|
|
5764
5663
|
}
|
|
5765
5664
|
|
|
5766
|
-
//
|
|
5665
|
+
// Import the plan-to-prd result sidecar into the authoritative SQL store
|
|
5666
|
+
// before marking the work item done.
|
|
5767
5667
|
// Must run BEFORE updateWorkItemStatus(DONE) — otherwise _retryCount is deleted and retries never advance
|
|
5768
5668
|
if (effectiveSuccess && type === WORK_TYPE.PLAN_TO_PRD && meta?.item?.id) {
|
|
5769
5669
|
let prdFound = false;
|
|
5770
5670
|
let resolvedPrdFilename = null;
|
|
5771
5671
|
const expectedFile = meta.item._prdFilename;
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
prdFound = true;
|
|
5786
|
-
resolvedPrdFilename = expectedFile;
|
|
5787
|
-
} else {
|
|
5788
|
-
log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but source_plan="${prd?.source_plan ?? '(unknown)'}" doesn't match expected planFile="${meta.item.planFile}" — possible collision, scanning by source_plan`);
|
|
5789
|
-
}
|
|
5790
|
-
} catch (e) {
|
|
5791
|
-
// Unparseable PRD file — log and accept (legacy behavior). A real
|
|
5792
|
-
// collision could hide behind a parse error, so the warn keeps the
|
|
5793
|
-
// anomaly visible for follow-up rather than failing silently.
|
|
5794
|
-
log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but failed to parse (${e.message}) — accepting as legacy behavior; manual inspection recommended if this recurs`);
|
|
5795
|
-
prdFound = true;
|
|
5796
|
-
resolvedPrdFilename = expectedFile;
|
|
5797
|
-
}
|
|
5798
|
-
} else {
|
|
5799
|
-
// No planFile to verify against — fall back to legacy existence check.
|
|
5800
|
-
prdFound = true;
|
|
5801
|
-
resolvedPrdFilename = expectedFile;
|
|
5672
|
+
const prdStore = require('./prd-store');
|
|
5673
|
+
const agentId = meta._agentId || meta.item.dispatched_to;
|
|
5674
|
+
const resultPath = agentId ? path.join(AGENTS_DIR, agentId, 'prd-result.json') : null;
|
|
5675
|
+
if (expectedFile && resultPath && fs.existsSync(resultPath)) {
|
|
5676
|
+
try {
|
|
5677
|
+
const envelope = shared.safeJsonNoRestore(resultPath);
|
|
5678
|
+
const filename = envelope?.filename || expectedFile;
|
|
5679
|
+
const prd = envelope?.prd || envelope;
|
|
5680
|
+
if (filename !== expectedFile) {
|
|
5681
|
+
throw new Error(`sidecar filename "${filename}" does not match expected "${expectedFile}"`);
|
|
5682
|
+
}
|
|
5683
|
+
if (!prd || !shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5684
|
+
throw new Error(`sidecar source_plan "${prd?.source_plan ?? '(unknown)'}" does not match "${meta.item.planFile}"`);
|
|
5802
5685
|
}
|
|
5686
|
+
prdStore.writePrd(path.join(PRD_DIR, expectedFile), prd);
|
|
5687
|
+
fs.unlinkSync(resultPath);
|
|
5688
|
+
} catch (e) {
|
|
5689
|
+
log('warn', `plan-to-prd ${meta.item.id}: could not import result sidecar: ${e.message}`);
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
for (const { filename, archived, plan } of prdStore.listPrdRows()) {
|
|
5693
|
+
if (!archived && filename === expectedFile
|
|
5694
|
+
&& (!meta.item.planFile || shared.prdMatchesSourcePlan(plan.source_plan, meta.item.planFile))) {
|
|
5695
|
+
prdFound = true;
|
|
5696
|
+
resolvedPrdFilename = filename;
|
|
5697
|
+
break;
|
|
5803
5698
|
}
|
|
5804
5699
|
}
|
|
5805
5700
|
if (!prdFound && meta.item.planFile) {
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
if (prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5812
|
-
prdFound = true;
|
|
5813
|
-
resolvedPrdFilename = f;
|
|
5814
|
-
break;
|
|
5815
|
-
}
|
|
5816
|
-
} catch {}
|
|
5701
|
+
for (const { filename, archived, plan } of prdStore.listPrdRows()) {
|
|
5702
|
+
if (!archived && shared.prdMatchesSourcePlan(plan.source_plan, meta.item.planFile)) {
|
|
5703
|
+
prdFound = true;
|
|
5704
|
+
resolvedPrdFilename = filename;
|
|
5705
|
+
break;
|
|
5817
5706
|
}
|
|
5818
|
-
}
|
|
5707
|
+
}
|
|
5819
5708
|
}
|
|
5820
5709
|
// Migrate _prdFilename when the source_plan scan found our PRD under a
|
|
5821
5710
|
// different on-disk filename (e.g. agent bumped past a clobbered name, or
|
|
@@ -5854,7 +5743,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5854
5743
|
// buildWorkItemDispatchVars()'s retry_context var (W-mrbq53ra000lf2e5) so the
|
|
5855
5744
|
// retried agent knows the PRD file was never found instead of silently
|
|
5856
5745
|
// repeating the same mistake.
|
|
5857
|
-
const gateReason = `Validation gate (#893): no PRD
|
|
5746
|
+
const gateReason = `Validation gate (#893): no PRD result sidecar was imported for planFile "${meta.item.planFile || w.planFile || '(unknown)'}" (expected filename "${expectedFile || '(unset)'}") after the previous attempt completed. Write agents/<agent-id>/prd-result.json exactly as specified in the playbook before finishing.`;
|
|
5858
5747
|
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
5859
5748
|
w.status = WI_STATUS.PENDING;
|
|
5860
5749
|
w._retryCount = retries + 1;
|
|
@@ -5866,13 +5755,13 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5866
5755
|
if (failedAgent) shared.bumpAgentRetryCount(w, failedAgent);
|
|
5867
5756
|
delete w.dispatched_at;
|
|
5868
5757
|
delete w.completedAt;
|
|
5869
|
-
log('warn', `plan-to-prd ${meta.item.id} completed without PRD
|
|
5758
|
+
log('warn', `plan-to-prd ${meta.item.id} completed without PRD result — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
|
|
5870
5759
|
} else {
|
|
5871
5760
|
w.status = WI_STATUS.FAILED;
|
|
5872
|
-
w.failReason = 'PRD
|
|
5761
|
+
w.failReason = 'PRD result not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
|
|
5873
5762
|
w.failedAt = ts();
|
|
5874
5763
|
w._lastRetryReason = gateReason;
|
|
5875
|
-
log('warn', `plan-to-prd ${meta.item.id} failed — no PRD
|
|
5764
|
+
log('warn', `plan-to-prd ${meta.item.id} failed — no PRD result after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
5876
5765
|
}
|
|
5877
5766
|
return data;
|
|
5878
5767
|
}, { skipWriteIfUnchanged: true });
|
|
@@ -6472,9 +6361,7 @@ function syncPrdFromPrs(config) {
|
|
|
6472
6361
|
// Paused, rejected, awaiting-approval, completed, and archived PRDs are
|
|
6473
6362
|
// skipped via _PRD_FROZEN_STATUSES. The locked write is idempotent.
|
|
6474
6363
|
function advancePrdStatusOnAllItemsDone(config) {
|
|
6475
|
-
|
|
6476
|
-
let prdFiles;
|
|
6477
|
-
try { prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
|
|
6364
|
+
const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
6478
6365
|
if (!prdFiles.length) return;
|
|
6479
6366
|
|
|
6480
6367
|
const allWorkItems = queries.getWorkItems(config);
|
|
@@ -6594,7 +6481,6 @@ function _verifyPrsNeedUpdate(existing, refMap) {
|
|
|
6594
6481
|
function persistVerifyPrsToPrd(config) {
|
|
6595
6482
|
try {
|
|
6596
6483
|
config = config || queries.getConfig();
|
|
6597
|
-
if (!fs.existsSync(PRD_DIR)) return;
|
|
6598
6484
|
const projects = shared.getProjects(config);
|
|
6599
6485
|
if (!projects.length) return;
|
|
6600
6486
|
|
|
@@ -6619,7 +6505,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6619
6505
|
for (const [planFile, refMap] of refsByPlan) {
|
|
6620
6506
|
// Resolve the PRD file (active or archived). sourcePlan is a bare filename.
|
|
6621
6507
|
const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
|
|
6622
|
-
const fpath = candidates.find(p =>
|
|
6508
|
+
const fpath = candidates.find(p => safeJsonNoRestore(p));
|
|
6623
6509
|
if (!fpath) continue;
|
|
6624
6510
|
|
|
6625
6511
|
// Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
|
|
@@ -7182,7 +7068,6 @@ module.exports = {
|
|
|
7182
7068
|
enforceVerifyPrContract,
|
|
7183
7069
|
markMissingPrAttachment,
|
|
7184
7070
|
parseCompletionReportFile,
|
|
7185
|
-
resolveHarnessPropagated,
|
|
7186
7071
|
// #308 — exported for unit testing: the final non-clean agent note now folds
|
|
7187
7072
|
// the grounded harness footprint in as a section instead of emitting a
|
|
7188
7073
|
// standalone harness-usage-* inbox note.
|
package/engine/llm.js
CHANGED
|
@@ -345,7 +345,7 @@ function _runtimeUnavailableResult(callOpts = {}) {
|
|
|
345
345
|
* Capability gating (matches engine.js _buildAgentSpawnFlags from P-2a6d9c4f):
|
|
346
346
|
* - effort/sessionId/maxBudget/bare/fallbackModel are dropped when the
|
|
347
347
|
* runtime's matching capability is false.
|
|
348
|
-
* - Copilot-specific opts (stream, disableBuiltinMcps,
|
|
348
|
+
* - Copilot-specific opts (stream, disableBuiltinMcps,
|
|
349
349
|
* reasoningSummaries) are emitted unconditionally; the Claude adapter
|
|
350
350
|
* ignores them via the "tolerate unknown opts" rule.
|
|
351
351
|
*/
|
|
@@ -365,7 +365,6 @@ function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
|
365
365
|
|
|
366
366
|
if (opts.stream === 'on' || opts.stream === 'off') flags.push('--stream', opts.stream);
|
|
367
367
|
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
368
|
-
if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
|
|
369
368
|
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
370
369
|
|
|
371
370
|
return flags;
|
|
@@ -387,7 +386,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
387
386
|
const {
|
|
388
387
|
direct, label, runtime, model, maxTurns, allowedTools, effort, sessionId,
|
|
389
388
|
maxBudget, bare, fallbackModel,
|
|
390
|
-
stream, disableBuiltinMcps,
|
|
389
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
391
390
|
images,
|
|
392
391
|
} = callOpts;
|
|
393
392
|
|
|
@@ -403,7 +402,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
403
402
|
const adapterOpts = {
|
|
404
403
|
model, maxTurns, allowedTools, effort, sessionId,
|
|
405
404
|
maxBudget, bare, fallbackModel,
|
|
406
|
-
stream, disableBuiltinMcps,
|
|
405
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
407
406
|
images,
|
|
408
407
|
tmpDir: llmTmpDir,
|
|
409
408
|
};
|
|
@@ -477,7 +476,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
477
476
|
const adapterFlags = _buildSpawnAgentFlags(runtime, {
|
|
478
477
|
model, maxTurns, allowedTools, effort, sessionId,
|
|
479
478
|
maxBudget, bare, fallbackModel,
|
|
480
|
-
stream, disableBuiltinMcps,
|
|
479
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
481
480
|
});
|
|
482
481
|
const args = [spawnScript, promptPath, sysPath, ...adapterFlags];
|
|
483
482
|
|
|
@@ -690,13 +689,12 @@ function _resolveModelForRuntime(runtime, callOpts) {
|
|
|
690
689
|
}
|
|
691
690
|
|
|
692
691
|
function _resolveRuntimeFeatureOpts({
|
|
693
|
-
stream, disableBuiltinMcps,
|
|
692
|
+
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
694
693
|
} = {}) {
|
|
695
694
|
const engine = engineConfig || {};
|
|
696
695
|
return {
|
|
697
696
|
stream: stream ?? engine.copilotStreamMode,
|
|
698
697
|
disableBuiltinMcps: disableBuiltinMcps ?? engine.copilotDisableBuiltinMcps,
|
|
699
|
-
suppressAgentsMd: suppressAgentsMd ?? engine.copilotSuppressAgentsMd,
|
|
700
698
|
reasoningSummaries: reasoningSummaries ?? engine.copilotReasoningSummaries,
|
|
701
699
|
};
|
|
702
700
|
}
|
|
@@ -759,7 +757,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
759
757
|
engineConfig,
|
|
760
758
|
// Cross-runtime + Copilot opts:
|
|
761
759
|
maxBudget, bare, fallbackModel,
|
|
762
|
-
stream, disableBuiltinMcps,
|
|
760
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
763
761
|
// P-7b2e4d01 — optional image attachments: [{ mimeType, dataBase64, filename? }]
|
|
764
762
|
images,
|
|
765
763
|
} = opts;
|
|
@@ -772,7 +770,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
772
770
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
773
771
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
774
772
|
const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
|
|
775
|
-
stream, disableBuiltinMcps,
|
|
773
|
+
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
776
774
|
});
|
|
777
775
|
|
|
778
776
|
let _abort = null;
|
|
@@ -884,7 +882,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
884
882
|
effort = null, direct = false,
|
|
885
883
|
model: modelOverride, cli: cliOverride, engineConfig,
|
|
886
884
|
maxBudget, bare, fallbackModel,
|
|
887
|
-
stream, disableBuiltinMcps,
|
|
885
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
888
886
|
// P-7b2e4d01 — optional image attachments: [{ mimeType, dataBase64, filename? }]
|
|
889
887
|
images,
|
|
890
888
|
} = opts;
|
|
@@ -897,7 +895,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
897
895
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
898
896
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
899
897
|
const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
|
|
900
|
-
stream, disableBuiltinMcps,
|
|
898
|
+
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
901
899
|
});
|
|
902
900
|
|
|
903
901
|
let _abort = null;
|
package/engine/managed-spawn.js
CHANGED
|
@@ -969,12 +969,7 @@ function removeManagedSpec(name) {
|
|
|
969
969
|
function listManagedSpecs(opts) {
|
|
970
970
|
opts = opts || {};
|
|
971
971
|
const statePath = _getStatePath();
|
|
972
|
-
|
|
973
|
-
try { raw = fs.readFileSync(statePath, 'utf8'); }
|
|
974
|
-
catch (_e) { return []; }
|
|
975
|
-
let parsed;
|
|
976
|
-
try { parsed = JSON.parse(raw); }
|
|
977
|
-
catch (_e) { return []; }
|
|
972
|
+
const parsed = shared.safeJsonObj(statePath);
|
|
978
973
|
const specs = (parsed && Array.isArray(parsed.specs)) ? parsed.specs : [];
|
|
979
974
|
if (opts.project) return specs.filter(s => s && s.owner_project === opts.project);
|
|
980
975
|
return specs.slice();
|
package/engine/memory-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// engine/memory-store.js — SQL-backed long-term memory records and retrieval.
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const { getDb, withTransaction } = require('./db');
|
|
4
|
+
const { getDb, withTransaction, prepareCached } = require('./db');
|
|
5
5
|
const { emitStateEvent } = require('./db-events');
|
|
6
6
|
|
|
7
7
|
const MEMORY_TYPES = new Set(['semantic', 'episodic', 'procedural']);
|
|
@@ -167,7 +167,8 @@ function upsertMemoryRecord(input) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function getMemoryRecord(id) {
|
|
170
|
-
|
|
170
|
+
const db = getDb();
|
|
171
|
+
return _row(prepareCached(db, 'SELECT * FROM memory_records WHERE id=?').get(String(id)));
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
function updateMemoryRecordState(id, action) {
|
|
@@ -219,7 +220,7 @@ function listMemoryRecords(filters = {}) {
|
|
|
219
220
|
}
|
|
220
221
|
const limit = Math.max(1, Math.min(500, Number(filters.limit) || 100));
|
|
221
222
|
const sql = `SELECT * FROM memory_records${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY updated_at DESC LIMIT ?`;
|
|
222
|
-
return getDb()
|
|
223
|
+
return prepareCached(getDb(), sql).all(...args, limit).map(_row);
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
function _matchQuery(query) {
|
|
@@ -268,14 +269,15 @@ function searchMemoryRecords(query, options = {}) {
|
|
|
268
269
|
}
|
|
269
270
|
const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
|
|
270
271
|
args.push(limit);
|
|
271
|
-
|
|
272
|
+
const sql = `
|
|
272
273
|
SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
|
|
273
274
|
FROM memory_records_fts
|
|
274
275
|
JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
275
276
|
WHERE ${where.join(' AND ')}
|
|
276
277
|
ORDER BY fts_rank
|
|
277
278
|
LIMIT ?
|
|
278
|
-
|
|
279
|
+
`;
|
|
280
|
+
return prepareCached(getDb(), sql).all(...args).map(_row);
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
function transitionMemoryRecord(id, status, options = {}) {
|
|
@@ -338,7 +340,7 @@ function recordRetrievalRun(run) {
|
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
function getRetrievalStats() {
|
|
341
|
-
return getDb()
|
|
343
|
+
return prepareCached(getDb(), `
|
|
342
344
|
SELECT COUNT(*) AS runs,
|
|
343
345
|
COALESCE(AVG(duration_ms), 0) AS avg_duration_ms,
|
|
344
346
|
COALESCE(AVG(selected_count), 0) AS avg_selected_count,
|