@yemi33/minions 0.1.2369 → 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/memory-search.js +112 -0
- package/dashboard/js/render-kb.js +15 -0
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +27 -27
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +148 -258
- 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/docs/team-memory.md +18 -4
- 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/consolidation.js +72 -1
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/017-agent-memory.js +208 -0
- 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/features.js +6 -0
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +141 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-retrieval.js +165 -0
- package/engine/memory-store.js +367 -0
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +64 -198
- 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 +179 -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
|
-
}
|
|
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;
|
|
1548
1532
|
|
|
1549
|
-
//
|
|
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
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// 3. Fall back to pr-links index (catches cases where prdItems wasn't
|
|
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
|
}
|
|
@@ -5389,6 +5313,81 @@ function applyGoalInvalidation(sourceWiId, invalidates, config) {
|
|
|
5389
5313
|
}
|
|
5390
5314
|
}
|
|
5391
5315
|
|
|
5316
|
+
function captureTaskMemoryEpisode(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, config) {
|
|
5317
|
+
if ((config?.engine?.memoryEpisodicCapture ?? ENGINE_DEFAULTS.memoryEpisodicCapture) !== true) return null;
|
|
5318
|
+
if (!dispatchItem || !dispatchItem.id) return null;
|
|
5319
|
+
if (structuredCompletion?.failure_class === FAILURE_CLASS.INJECTION_FLAGGED) return null;
|
|
5320
|
+
|
|
5321
|
+
const meta = dispatchItem.meta || {};
|
|
5322
|
+
const item = meta.item || {};
|
|
5323
|
+
const project = meta.project?.name
|
|
5324
|
+
|| (item._source && item._source !== 'central' ? item._source : '')
|
|
5325
|
+
|| dispatchItem.project
|
|
5326
|
+
|| '';
|
|
5327
|
+
const files = structuredCompletion?.files_changed
|
|
5328
|
+
|| structuredCompletion?.affected_files
|
|
5329
|
+
|| [];
|
|
5330
|
+
const redact = typeof shared.redactSecrets === 'function' ? shared.redactSecrets : String;
|
|
5331
|
+
const fileList = (Array.isArray(files) ? files : String(files || '').split(','))
|
|
5332
|
+
.map(value => redact(String(value).trim()).slice(0, 300))
|
|
5333
|
+
.filter(Boolean)
|
|
5334
|
+
.slice(0, 20);
|
|
5335
|
+
const rawTests = structuredCompletion?.tests || 'N/A';
|
|
5336
|
+
const tests = Array.isArray(rawTests)
|
|
5337
|
+
? rawTests.slice(0, 20).map(value => redact(String(value)).slice(0, 500))
|
|
5338
|
+
: redact(String(rawTests)).slice(0, 2000);
|
|
5339
|
+
const pr = redact(String(structuredCompletion?.pr || item._prUrl || item._pr || 'N/A')).slice(0, 1000);
|
|
5340
|
+
const failureClass = redact(String(structuredCompletion?.failure_class || 'N/A')).slice(0, 200);
|
|
5341
|
+
const safeSummary = redact(String(resultSummary || structuredCompletion?.summary || 'No summary reported')).slice(0, 4000);
|
|
5342
|
+
const title = redact(String(item.title || item.name || dispatchItem.title || item.id || dispatchItem.id)).slice(0, 500);
|
|
5343
|
+
const body = [
|
|
5344
|
+
`Outcome: ${outcome}`,
|
|
5345
|
+
`Work item: ${item.id || 'N/A'}`,
|
|
5346
|
+
`Work type: ${dispatchItem.type || item.type || 'N/A'}`,
|
|
5347
|
+
`Agent: ${agentId || dispatchItem.agent || 'unknown'}`,
|
|
5348
|
+
`Project: ${project || 'N/A'}`,
|
|
5349
|
+
`PR: ${pr}`,
|
|
5350
|
+
`Tests: ${Array.isArray(tests) ? tests.join(', ') : tests}`,
|
|
5351
|
+
`Failure class: ${failureClass}`,
|
|
5352
|
+
fileList.length ? `Files: ${fileList.join(', ')}` : 'Files: N/A',
|
|
5353
|
+
'',
|
|
5354
|
+
'Summary:',
|
|
5355
|
+
safeSummary,
|
|
5356
|
+
].join('\n');
|
|
5357
|
+
|
|
5358
|
+
try {
|
|
5359
|
+
return require('./memory-store').upsertMemoryRecord({
|
|
5360
|
+
memoryType: 'episodic',
|
|
5361
|
+
scopeType: project ? 'project' : (agentId ? 'agent' : 'global'),
|
|
5362
|
+
scopeKey: project || agentId || '',
|
|
5363
|
+
title: `${outcome}: ${title}`,
|
|
5364
|
+
body,
|
|
5365
|
+
tags: ['task-episode', dispatchItem.type || item.type || 'unknown', outcome, ...fileList.slice(0, 12)],
|
|
5366
|
+
sourceType: 'dispatch',
|
|
5367
|
+
sourceRef: String(dispatchItem.id),
|
|
5368
|
+
sourcePath: structuredCompletion?._path || null,
|
|
5369
|
+
trust: 'system',
|
|
5370
|
+
importance: outcome === 'failed' ? 0.85 : (outcome === 'partial' ? 0.75 : 0.6),
|
|
5371
|
+
confidence: structuredCompletion ? 0.8 : 0.55,
|
|
5372
|
+
metadata: {
|
|
5373
|
+
dispatchId: dispatchItem.id,
|
|
5374
|
+
workItemId: item.id || null,
|
|
5375
|
+
agent: agentId || dispatchItem.agent || null,
|
|
5376
|
+
project: project || null,
|
|
5377
|
+
outcome,
|
|
5378
|
+
workType: dispatchItem.type || item.type || null,
|
|
5379
|
+
pr,
|
|
5380
|
+
tests,
|
|
5381
|
+
failureClass,
|
|
5382
|
+
files: fileList,
|
|
5383
|
+
},
|
|
5384
|
+
});
|
|
5385
|
+
} catch (err) {
|
|
5386
|
+
log('warn', `Task memory episode write failed for ${dispatchItem.id}: ${err.message}`);
|
|
5387
|
+
return null;
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5392
5391
|
async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, opts) {
|
|
5393
5392
|
|
|
5394
5393
|
const detectPhantom = !!(opts && opts.detectPhantom);
|
|
@@ -5484,31 +5483,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5484
5483
|
structuredCompletion.status = 'failed-injection-flagged';
|
|
5485
5484
|
}
|
|
5486
5485
|
}
|
|
5487
|
-
// P-c6f5e8b3 — harness grounding cross-check (Stage 2). Cross-check the
|
|
5488
|
-
// agent's self-reported `harnessUsed` against the spawn-time
|
|
5489
|
-
// `_harnessPropagated` manifest and annotate each entry `grounded:true|false`.
|
|
5490
|
-
// The grounded result replaces `structuredCompletion.harnessUsed` IN PLACE, so
|
|
5491
|
-
// it rides the existing `structuredCompletion` storage onto the completed
|
|
5492
|
-
// dispatch record (completeDispatch persists `item.structuredCompletion`) for
|
|
5493
|
-
// the Stage-3 surfaces (PR comment / final agent note / WI modal) to read.
|
|
5494
|
-
// Pure, non-destructive (grounded:false entries are kept + flagged, never
|
|
5495
|
-
// dropped), and best-effort: any failure here must never block completion.
|
|
5496
|
-
//
|
|
5497
|
-
// #308 — the grounded footprint is no longer routed to its own
|
|
5498
|
-
// `harness-usage-*` inbox note (that produced one duplicate-looking note chip
|
|
5499
|
-
// per attempt). For clean completions the structured `_harnessUsed` WI-modal
|
|
5500
|
-
// UI surfaces it; for non-clean outcomes writeNonCleanAgentReport folds the
|
|
5501
|
-
// same buildHarnessUsedSection block into the single final agent note.
|
|
5502
|
-
if (structuredCompletion && structuredCompletion.harnessUsed) {
|
|
5503
|
-
try {
|
|
5504
|
-
const propagated = resolveHarnessPropagated(dispatchItem);
|
|
5505
|
-
const grounded = shared.groundHarnessUsed(structuredCompletion.harnessUsed, propagated);
|
|
5506
|
-
structuredCompletion.harnessUsed = grounded; // canonical grounded shape, or null when nothing survived
|
|
5507
|
-
} catch (err) {
|
|
5508
|
-
log('warn', `Harness grounding cross-check failed for ${dispatchItem.id} (non-fatal): ${err.message}`);
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
|
|
5512
5486
|
const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
|
|
5513
5487
|
|
|
5514
5488
|
// Save session for potential resume on next dispatch
|
|
@@ -5577,6 +5551,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5577
5551
|
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
5578
5552
|
}
|
|
5579
5553
|
const effectiveSuccess = !nonceMismatch && ((isSuccess && !agentReportedFailure) || autoRecovered);
|
|
5554
|
+
if (!nonceMismatch) {
|
|
5555
|
+
const episodeOutcome = effectiveSuccess
|
|
5556
|
+
? (autoRecovered || completionStatus.startsWith('partial') ? 'partial' : 'success')
|
|
5557
|
+
: 'failed';
|
|
5558
|
+
captureTaskMemoryEpisode(dispatchItem, agentId, episodeOutcome, structuredCompletion, completionGateSummary, config);
|
|
5559
|
+
}
|
|
5580
5560
|
|
|
5581
5561
|
let nonCleanReportWritten = false;
|
|
5582
5562
|
if (completionStatus.startsWith('partial') || autoRecovered || (agentReportedFailure && isSuccess)) {
|
|
@@ -5682,59 +5662,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5682
5662
|
}
|
|
5683
5663
|
}
|
|
5684
5664
|
|
|
5685
|
-
//
|
|
5665
|
+
// Import the plan-to-prd result sidecar into the authoritative SQL store
|
|
5666
|
+
// before marking the work item done.
|
|
5686
5667
|
// Must run BEFORE updateWorkItemStatus(DONE) — otherwise _retryCount is deleted and retries never advance
|
|
5687
5668
|
if (effectiveSuccess && type === WORK_TYPE.PLAN_TO_PRD && meta?.item?.id) {
|
|
5688
5669
|
let prdFound = false;
|
|
5689
5670
|
let resolvedPrdFilename = null;
|
|
5690
5671
|
const expectedFile = meta.item._prdFilename;
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
prdFound = true;
|
|
5705
|
-
resolvedPrdFilename = expectedFile;
|
|
5706
|
-
} else {
|
|
5707
|
-
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`);
|
|
5708
|
-
}
|
|
5709
|
-
} catch (e) {
|
|
5710
|
-
// Unparseable PRD file — log and accept (legacy behavior). A real
|
|
5711
|
-
// collision could hide behind a parse error, so the warn keeps the
|
|
5712
|
-
// anomaly visible for follow-up rather than failing silently.
|
|
5713
|
-
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`);
|
|
5714
|
-
prdFound = true;
|
|
5715
|
-
resolvedPrdFilename = expectedFile;
|
|
5716
|
-
}
|
|
5717
|
-
} else {
|
|
5718
|
-
// No planFile to verify against — fall back to legacy existence check.
|
|
5719
|
-
prdFound = true;
|
|
5720
|
-
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}"`);
|
|
5721
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;
|
|
5722
5698
|
}
|
|
5723
5699
|
}
|
|
5724
5700
|
if (!prdFound && meta.item.planFile) {
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
if (prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5731
|
-
prdFound = true;
|
|
5732
|
-
resolvedPrdFilename = f;
|
|
5733
|
-
break;
|
|
5734
|
-
}
|
|
5735
|
-
} 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;
|
|
5736
5706
|
}
|
|
5737
|
-
}
|
|
5707
|
+
}
|
|
5738
5708
|
}
|
|
5739
5709
|
// Migrate _prdFilename when the source_plan scan found our PRD under a
|
|
5740
5710
|
// different on-disk filename (e.g. agent bumped past a clobbered name, or
|
|
@@ -5773,7 +5743,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5773
5743
|
// buildWorkItemDispatchVars()'s retry_context var (W-mrbq53ra000lf2e5) so the
|
|
5774
5744
|
// retried agent knows the PRD file was never found instead of silently
|
|
5775
5745
|
// repeating the same mistake.
|
|
5776
|
-
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.`;
|
|
5777
5747
|
if (retries < ENGINE_DEFAULTS.maxRetries) {
|
|
5778
5748
|
w.status = WI_STATUS.PENDING;
|
|
5779
5749
|
w._retryCount = retries + 1;
|
|
@@ -5785,13 +5755,13 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5785
5755
|
if (failedAgent) shared.bumpAgentRetryCount(w, failedAgent);
|
|
5786
5756
|
delete w.dispatched_at;
|
|
5787
5757
|
delete w.completedAt;
|
|
5788
|
-
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}`);
|
|
5789
5759
|
} else {
|
|
5790
5760
|
w.status = WI_STATUS.FAILED;
|
|
5791
|
-
w.failReason = 'PRD
|
|
5761
|
+
w.failReason = 'PRD result not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
|
|
5792
5762
|
w.failedAt = ts();
|
|
5793
5763
|
w._lastRetryReason = gateReason;
|
|
5794
|
-
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`);
|
|
5795
5765
|
}
|
|
5796
5766
|
return data;
|
|
5797
5767
|
}, { skipWriteIfUnchanged: true });
|
|
@@ -6391,9 +6361,7 @@ function syncPrdFromPrs(config) {
|
|
|
6391
6361
|
// Paused, rejected, awaiting-approval, completed, and archived PRDs are
|
|
6392
6362
|
// skipped via _PRD_FROZEN_STATUSES. The locked write is idempotent.
|
|
6393
6363
|
function advancePrdStatusOnAllItemsDone(config) {
|
|
6394
|
-
|
|
6395
|
-
let prdFiles;
|
|
6396
|
-
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);
|
|
6397
6365
|
if (!prdFiles.length) return;
|
|
6398
6366
|
|
|
6399
6367
|
const allWorkItems = queries.getWorkItems(config);
|
|
@@ -6513,7 +6481,6 @@ function _verifyPrsNeedUpdate(existing, refMap) {
|
|
|
6513
6481
|
function persistVerifyPrsToPrd(config) {
|
|
6514
6482
|
try {
|
|
6515
6483
|
config = config || queries.getConfig();
|
|
6516
|
-
if (!fs.existsSync(PRD_DIR)) return;
|
|
6517
6484
|
const projects = shared.getProjects(config);
|
|
6518
6485
|
if (!projects.length) return;
|
|
6519
6486
|
|
|
@@ -6538,7 +6505,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6538
6505
|
for (const [planFile, refMap] of refsByPlan) {
|
|
6539
6506
|
// Resolve the PRD file (active or archived). sourcePlan is a bare filename.
|
|
6540
6507
|
const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
|
|
6541
|
-
const fpath = candidates.find(p =>
|
|
6508
|
+
const fpath = candidates.find(p => safeJsonNoRestore(p));
|
|
6542
6509
|
if (!fpath) continue;
|
|
6543
6510
|
|
|
6544
6511
|
// Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
|
|
@@ -7101,11 +7068,12 @@ module.exports = {
|
|
|
7101
7068
|
enforceVerifyPrContract,
|
|
7102
7069
|
markMissingPrAttachment,
|
|
7103
7070
|
parseCompletionReportFile,
|
|
7104
|
-
resolveHarnessPropagated,
|
|
7105
7071
|
// #308 — exported for unit testing: the final non-clean agent note now folds
|
|
7106
7072
|
// the grounded harness footprint in as a section instead of emitting a
|
|
7107
7073
|
// standalone harness-usage-* inbox note.
|
|
7108
7074
|
writeNonCleanAgentReport,
|
|
7075
|
+
// exported for testing
|
|
7076
|
+
captureTaskMemoryEpisode,
|
|
7109
7077
|
normalizeCompletionArtifacts,
|
|
7110
7078
|
completionArtifactToNoteEntry,
|
|
7111
7079
|
mergeArtifactNotes,
|
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();
|