@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/dashboard.js
CHANGED
|
@@ -65,6 +65,7 @@ const prTrack = require('./engine/pr-track');
|
|
|
65
65
|
const features = require('./engine/features');
|
|
66
66
|
const ccWorkerPool = require('./engine/cc-worker-pool');
|
|
67
67
|
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
68
|
+
const memoryStore = require('./engine/memory-store');
|
|
68
69
|
const os = require('os');
|
|
69
70
|
|
|
70
71
|
const { safeRead, safeReadOrNull, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore, safeUnlink, mutateJsonFileLocked, mutateTextFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, WORKTREE_REQUIRING_TYPES, reopenWorkItem } = shared;
|
|
@@ -81,24 +82,6 @@ const IS_DEV_MODE = fs.existsSync(path.join(MINIONS_DIR, '.git'));
|
|
|
81
82
|
const FAVICON_EMOJI = IS_DEV_MODE ? '🚧' : '👽';
|
|
82
83
|
const TITLE_SUFFIX = IS_DEV_MODE ? ' [DEV]' : '';
|
|
83
84
|
|
|
84
|
-
// Startup size guard (#1167): fail fast with a clear error when dispatch.json /
|
|
85
|
-
// cooldowns.json have ballooned past ENGINE_DEFAULTS.maxStateFileBytes. Without
|
|
86
|
-
// this, V8 silently OOMs on JSON.parse(~1 GB) and the operator has no hint as to
|
|
87
|
-
// which file is bloated. The thrown error names the file and directs to
|
|
88
|
-
// engine/contexts/ where sidecars live.
|
|
89
|
-
(() => {
|
|
90
|
-
const stateFiles = [
|
|
91
|
-
DISPATCH_PATH,
|
|
92
|
-
path.join(ENGINE_DIR, 'cooldowns.json'),
|
|
93
|
-
];
|
|
94
|
-
for (const fp of stateFiles) {
|
|
95
|
-
try { shared.assertStateFileSize(fp); } catch (e) {
|
|
96
|
-
console.error('\n[dashboard] STARTUP ABORTED — ' + e.message + '\n');
|
|
97
|
-
process.exit(78); // 78 = configuration error; consistent with spawn-agent.js
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
})();
|
|
101
|
-
|
|
102
85
|
let CONFIG = queries.getConfig();
|
|
103
86
|
let PROJECTS = _getProjects(CONFIG);
|
|
104
87
|
// PORT resolution chain (W-mq5nwl9l): explicit CLI arg (argv[2]) > env
|
|
@@ -468,6 +451,18 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
468
451
|
// `worktreeMode`. Drop any stale legacy key on every settings save so a
|
|
469
452
|
// migrated project never carries both fields.
|
|
470
453
|
delete currentProject.worktreeMode;
|
|
454
|
+
// W-mrchrfe100067d3a — mirror the per-project additionalRepoScopes
|
|
455
|
+
// allowlist the same way: present on candidate → copy; absent → clear so
|
|
456
|
+
// the endpoint falls back to single-scope matching. Without this branch
|
|
457
|
+
// the validated update (set/deleted on `candidate` in
|
|
458
|
+
// handleSettingsUpdate) is silently dropped before reaching disk — the
|
|
459
|
+
// endpoint would return 200 but persist nothing (issue: fix before
|
|
460
|
+
// configuring minions-opg to poll the secondary yemi33/minions scope).
|
|
461
|
+
if (Object.prototype.hasOwnProperty.call(candidateProject, 'additionalRepoScopes')) {
|
|
462
|
+
currentProject.additionalRepoScopes = candidateProject.additionalRepoScopes;
|
|
463
|
+
} else {
|
|
464
|
+
delete currentProject.additionalRepoScopes;
|
|
465
|
+
}
|
|
471
466
|
}
|
|
472
467
|
}
|
|
473
468
|
shared.pruneDefaultClaudeConfig(current);
|
|
@@ -578,6 +573,9 @@ function normalizePrMetadata(metadata) {
|
|
|
578
573
|
description: typeof metadata.description === 'string' ? metadata.description : '',
|
|
579
574
|
branch: typeof metadata.branch === 'string' ? metadata.branch.trim().replace(/^refs\/heads\//i, '') : '',
|
|
580
575
|
author: typeof metadata.author === 'string' ? metadata.author.trim() : '',
|
|
576
|
+
// W-mrezh0yb0007f733 — carry the platform's real creation timestamp through
|
|
577
|
+
// so the manual-link path can seed `created` with it instead of now().
|
|
578
|
+
created: typeof metadata.created === 'string' ? metadata.created.trim() : '',
|
|
581
579
|
};
|
|
582
580
|
}
|
|
583
581
|
|
|
@@ -1364,7 +1362,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
|
|
|
1364
1362
|
branch: metadata?.branch || '',
|
|
1365
1363
|
reviewStatus: 'pending',
|
|
1366
1364
|
status: 'active',
|
|
1367
|
-
|
|
1365
|
+
// W-mrezh0yb0007f733 — prefer the platform's real creationDate (fetched into
|
|
1366
|
+
// metadata for ADO before linking) over link-time now(). When it isn't
|
|
1367
|
+
// available yet (e.g. GitHub, where enrichment runs async after this insert,
|
|
1368
|
+
// or an ADO metadata-fetch failure), fall back to now() — the next PR poll
|
|
1369
|
+
// corrects it from the platform via shared.shouldAdoptPlatformCreated.
|
|
1370
|
+
created: metadata?.created || new Date().toISOString(),
|
|
1368
1371
|
url,
|
|
1369
1372
|
prdItems: linkedWorkItemId ? [linkedWorkItemId] : [],
|
|
1370
1373
|
contextOnly: resolvedContextOnly,
|
|
@@ -1498,161 +1501,10 @@ function _resolveSkillReadPath({ file, dir, source, config, skillFiles } = {}) {
|
|
|
1498
1501
|
return null;
|
|
1499
1502
|
}
|
|
1500
1503
|
|
|
1501
|
-
// ──
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
// and the canonical footgun: per-project assets that live as uncommitted files
|
|
1506
|
-
// in the operator's main checkout but won't propagate to fresh worktrees.
|
|
1507
|
-
//
|
|
1508
|
-
// Surfaced via GET /api/harness/diagnostics and rendered on the Tools page so
|
|
1509
|
-
// operators can spot a missing or unpropagated harness asset without leaving
|
|
1510
|
-
// the dashboard. See docs/harness-propagation.md for the full contract.
|
|
1511
|
-
const HARNESS_FOOTGUN_DIRS = [
|
|
1512
|
-
'.claude/skills',
|
|
1513
|
-
'.claude/commands',
|
|
1514
|
-
'.copilot/skills',
|
|
1515
|
-
'.copilot/commands',
|
|
1516
|
-
'.agents/skills',
|
|
1517
|
-
'.agents/commands',
|
|
1518
|
-
];
|
|
1519
|
-
const HARNESS_FOOTGUN_FILES = [
|
|
1520
|
-
'.claude/.mcp.json',
|
|
1521
|
-
'.copilot/.mcp.json',
|
|
1522
|
-
'.mcp.json',
|
|
1523
|
-
];
|
|
1524
|
-
|
|
1525
|
-
const _PROJECT_LOCAL_FOOTGUN_WARNING =
|
|
1526
|
-
'These files exist in your main checkout but are uncommitted or untracked in ' +
|
|
1527
|
-
'git. A fresh `git worktree add` for a mutating dispatch will NOT see them, so ' +
|
|
1528
|
-
'the dispatched agent silently underperforms. Fix: commit them, move them to ' +
|
|
1529
|
-
'user scope (~/.claude/skills/..., ~/.copilot/skills/...), or flip the project ' +
|
|
1530
|
-
'to live-checkout mode (checkoutMode: live).';
|
|
1531
|
-
|
|
1532
|
-
function _walkUncommittedHarnessAssets(absStart, rootDir, projectName) {
|
|
1533
|
-
const results = [];
|
|
1534
|
-
const stack = [absStart];
|
|
1535
|
-
let count = 0;
|
|
1536
|
-
while (stack.length > 0 && count < 500) {
|
|
1537
|
-
const cur = stack.pop();
|
|
1538
|
-
let entries;
|
|
1539
|
-
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
1540
|
-
catch { continue; }
|
|
1541
|
-
for (const ent of entries) {
|
|
1542
|
-
if (ent.name === '.git' || ent.name === 'node_modules') continue;
|
|
1543
|
-
const full = path.join(cur, ent.name);
|
|
1544
|
-
if (ent.isDirectory()) { stack.push(full); continue; }
|
|
1545
|
-
if (!ent.isFile()) continue;
|
|
1546
|
-
const rel = path.relative(rootDir, full);
|
|
1547
|
-
const normalized = rel.replace(/\\/g, '/');
|
|
1548
|
-
let kind = null;
|
|
1549
|
-
if (
|
|
1550
|
-
normalized.startsWith('.claude/skills/') ||
|
|
1551
|
-
normalized.startsWith('.copilot/skills/') ||
|
|
1552
|
-
normalized.startsWith('.agents/skills/')
|
|
1553
|
-
) kind = 'skill';
|
|
1554
|
-
else if (
|
|
1555
|
-
normalized.startsWith('.claude/commands/') ||
|
|
1556
|
-
normalized.startsWith('.copilot/commands/') ||
|
|
1557
|
-
normalized.startsWith('.agents/commands/')
|
|
1558
|
-
) kind = 'command';
|
|
1559
|
-
else if (
|
|
1560
|
-
normalized === '.mcp.json' ||
|
|
1561
|
-
normalized === '.claude/.mcp.json' ||
|
|
1562
|
-
normalized === '.copilot/.mcp.json'
|
|
1563
|
-
) kind = 'mcp';
|
|
1564
|
-
if (!kind) continue;
|
|
1565
|
-
results.push({ file: rel, kind, scope: `project:${projectName}` });
|
|
1566
|
-
count++;
|
|
1567
|
-
if (count >= 500) break;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return results;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function _scanProjectLocalHarnessFootgun(project) {
|
|
1574
|
-
const out = {
|
|
1575
|
-
project: project && project.name ? project.name : '',
|
|
1576
|
-
localPath: project && project.localPath ? project.localPath : '',
|
|
1577
|
-
uncommittedAssets: [],
|
|
1578
|
-
footgunWarning: _PROJECT_LOCAL_FOOTGUN_WARNING,
|
|
1579
|
-
};
|
|
1580
|
-
if (!project || !project.localPath) return out;
|
|
1581
|
-
// Skip the synchronous `git status` for UNC / WSL-from-Windows paths
|
|
1582
|
-
// (\\wsl.localhost\..., \\wsl$\..., or any \\server\share). git/stat over a
|
|
1583
|
-
// WSL UNC mount can hang for minutes; even capped at the 10s timeout below it
|
|
1584
|
-
// hard-blocks the dashboard event loop on every poll, freezing the whole
|
|
1585
|
-
// dashboard. Such projects can't carry uncommitted project-local harness
|
|
1586
|
-
// assets the engine would propagate anyway, so returning the empty shell here
|
|
1587
|
-
// is correct, not lossy. (Load-reduction audit 2026-06-24.)
|
|
1588
|
-
const _lp = String(project.localPath);
|
|
1589
|
-
if (_lp.startsWith('\\\\') || _lp.startsWith('//')) return out;
|
|
1590
|
-
let raw = '';
|
|
1591
|
-
try {
|
|
1592
|
-
const { execFileSync } = require('child_process');
|
|
1593
|
-
raw = execFileSync('git', ['status', '--porcelain', '-z'], {
|
|
1594
|
-
cwd: project.localPath,
|
|
1595
|
-
encoding: 'utf8',
|
|
1596
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1597
|
-
timeout: 10000,
|
|
1598
|
-
// W-mqecdoot — MUST set windowsHide. This runs per-project on every
|
|
1599
|
-
// GET /api/harness/diagnostics hit (Tools/Harness page, polled), and a
|
|
1600
|
-
// detached dashboard has no console — so without this each call pops a
|
|
1601
|
-
// visible git.exe console window per project ("infinite git windows").
|
|
1602
|
-
// This was the only git spawn in the codebase missing windowsHide.
|
|
1603
|
-
windowsHide: true,
|
|
1604
|
-
});
|
|
1605
|
-
} catch { return out; }
|
|
1606
|
-
if (!raw) return out;
|
|
1607
|
-
const records = raw.split('\0').filter(Boolean);
|
|
1608
|
-
const seen = new Set();
|
|
1609
|
-
for (const rec of records) {
|
|
1610
|
-
if (rec.length < 4) continue;
|
|
1611
|
-
const filePath = rec.slice(3);
|
|
1612
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
1613
|
-
const isDirRecord = normalized.endsWith('/');
|
|
1614
|
-
const trimmed = isDirRecord ? normalized.slice(0, -1) : normalized;
|
|
1615
|
-
let overlaps = false;
|
|
1616
|
-
for (const dir of HARNESS_FOOTGUN_DIRS) {
|
|
1617
|
-
if (
|
|
1618
|
-
trimmed === dir ||
|
|
1619
|
-
trimmed.startsWith(dir + '/') ||
|
|
1620
|
-
dir.startsWith(trimmed + '/')
|
|
1621
|
-
) { overlaps = true; break; }
|
|
1622
|
-
}
|
|
1623
|
-
if (!overlaps) {
|
|
1624
|
-
for (const file of HARNESS_FOOTGUN_FILES) {
|
|
1625
|
-
if (trimmed === file || file.startsWith(trimmed + '/')) { overlaps = true; break; }
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
if (!overlaps) continue;
|
|
1629
|
-
|
|
1630
|
-
const absStart = path.join(project.localPath, filePath);
|
|
1631
|
-
let stat;
|
|
1632
|
-
try { stat = fs.statSync(absStart); } catch { continue; }
|
|
1633
|
-
if (stat.isDirectory()) {
|
|
1634
|
-
for (const asset of _walkUncommittedHarnessAssets(absStart, project.localPath, out.project)) {
|
|
1635
|
-
if (seen.has(asset.file)) continue;
|
|
1636
|
-
seen.add(asset.file);
|
|
1637
|
-
out.uncommittedAssets.push(asset);
|
|
1638
|
-
}
|
|
1639
|
-
} else {
|
|
1640
|
-
let kind = 'other';
|
|
1641
|
-
if (normalized.includes('/skills/')) kind = 'skill';
|
|
1642
|
-
else if (normalized.includes('/commands/')) kind = 'command';
|
|
1643
|
-
else if (normalized.endsWith('.mcp.json')) kind = 'mcp';
|
|
1644
|
-
if (seen.has(filePath)) continue;
|
|
1645
|
-
seen.add(filePath);
|
|
1646
|
-
out.uncommittedAssets.push({ file: filePath, kind, scope: `project:${out.project}` });
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
return out;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// TTL cache for the harness diagnostic. The build runs a synchronous `git
|
|
1653
|
-
// status` + directory walks per project; the Tools page polls it while open,
|
|
1654
|
-
// so without a cache it re-runs the whole sweep every 4s and blocks the event
|
|
1655
|
-
// loop each time. The diagnostic is rare-change, so a short TTL is ample.
|
|
1504
|
+
// ── Runtime harness inventory ─────────────────────────────────────────────────
|
|
1505
|
+
// Read-only view of native skill, command, and MCP locations declared by each
|
|
1506
|
+
// adapter. It does not influence spawning or attest what a runtime loaded.
|
|
1507
|
+
// The inventory is rare-change, so a short TTL is ample.
|
|
1656
1508
|
// Bypassed whenever explicit opts are supplied (unit tests pass homeDir/
|
|
1657
1509
|
// projects/engineConfig) so test determinism is unaffected.
|
|
1658
1510
|
// (Load-reduction audit 2026-06-24.)
|
|
@@ -1729,29 +1581,7 @@ function _buildHarnessDiagnosticsUncached(opts = {}) {
|
|
|
1729
1581
|
collectMissing(mcpConfigPaths, runtimeName, 'mcp');
|
|
1730
1582
|
}
|
|
1731
1583
|
|
|
1732
|
-
let addDirSnapshot = [];
|
|
1733
|
-
if (preflight && registry) {
|
|
1734
|
-
let fleetRuntime = null;
|
|
1735
|
-
try { fleetRuntime = registry.resolveRuntime(fleetDefaultCli); }
|
|
1736
|
-
catch { fleetRuntime = null; }
|
|
1737
|
-
if (fleetRuntime) {
|
|
1738
|
-
const snapshot = preflight._computeAddDirSnapshot(fleetRuntime, {
|
|
1739
|
-
minionsHome: MINIONS_DIR,
|
|
1740
|
-
homeDir,
|
|
1741
|
-
existsFn,
|
|
1742
|
-
});
|
|
1743
|
-
if (Array.isArray(snapshot)) addDirSnapshot = snapshot.map(slimRow);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
1584
|
const suppressed = [];
|
|
1748
|
-
if (engineConfig.copilotSuppressAgentsMd !== false) {
|
|
1749
|
-
suppressed.push({
|
|
1750
|
-
flag: 'copilotSuppressAgentsMd',
|
|
1751
|
-
value: true,
|
|
1752
|
-
effect: 'Copilot --no-custom-instructions: in-tree AGENTS.md is NOT auto-loaded, so Minions playbook prompts are not overridden by repo-local instructions.',
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
1585
|
if (engineConfig.copilotDisableBuiltinMcps !== false) {
|
|
1756
1586
|
suppressed.push({
|
|
1757
1587
|
flag: 'copilotDisableBuiltinMcps',
|
|
@@ -1759,22 +1589,10 @@ function _buildHarnessDiagnosticsUncached(opts = {}) {
|
|
|
1759
1589
|
effect: 'Copilot --disable-builtin-mcps: bundled github-mcp-server is stripped so dispatched agents do not open a parallel PR alongside the Minions PR.',
|
|
1760
1590
|
});
|
|
1761
1591
|
}
|
|
1762
|
-
if (engineConfig.hermeticHarness === true) {
|
|
1763
|
-
suppressed.push({
|
|
1764
|
-
flag: 'hermeticHarness',
|
|
1765
|
-
value: true,
|
|
1766
|
-
effect: 'Hermetic harness mode: only the engine playbook + system prompt are surfaced; user-scope skills, commands, and MCP configs are NOT propagated to this dispatch.',
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
const projectLocalOnMain = projects.map(_scanProjectLocalHarnessFootgun);
|
|
1771
|
-
|
|
1772
1592
|
return {
|
|
1773
1593
|
fleetDefaultCli,
|
|
1774
1594
|
runtimes,
|
|
1775
|
-
addDirSnapshot,
|
|
1776
1595
|
suppressed,
|
|
1777
|
-
projectLocalOnMain,
|
|
1778
1596
|
missingDirs,
|
|
1779
1597
|
};
|
|
1780
1598
|
}
|
|
@@ -1840,6 +1658,11 @@ let _plansCacheTs = 0;
|
|
|
1840
1658
|
const PLANS_CACHE_TTL_MS = 5000;
|
|
1841
1659
|
function invalidatePlansCache() { _plansCache = null; _plansCacheTs = 0; }
|
|
1842
1660
|
|
|
1661
|
+
function statePathExists(filePath) {
|
|
1662
|
+
if (prdStore.parsePrdPath(filePath)) return prdStore.readPrd(filePath) != null;
|
|
1663
|
+
return fs.existsSync(filePath);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1843
1666
|
// Resolve a plan/PRD file path: .json files live in prd/, .md files in plans/
|
|
1844
1667
|
// Validates that the file stays within the expected directory to prevent path traversal.
|
|
1845
1668
|
function resolvePlanPath(file) {
|
|
@@ -1847,11 +1670,12 @@ function resolvePlanPath(file) {
|
|
|
1847
1670
|
// Validate against both prd/ and prd/archive/
|
|
1848
1671
|
shared.sanitizePath(file, PRD_DIR);
|
|
1849
1672
|
const active = path.join(PRD_DIR, file);
|
|
1850
|
-
if (
|
|
1673
|
+
if (prdStore.readPrd(active)) return active;
|
|
1851
1674
|
const archived = path.join(PRD_DIR, 'archive', file);
|
|
1852
|
-
if (
|
|
1675
|
+
if (prdStore.readPrd(archived)) return archived;
|
|
1853
1676
|
return active;
|
|
1854
1677
|
}
|
|
1678
|
+
|
|
1855
1679
|
// Validate against both plans/ and plans/archive/
|
|
1856
1680
|
shared.sanitizePath(file, PLANS_DIR);
|
|
1857
1681
|
const active = path.join(PLANS_DIR, file);
|
|
@@ -1880,7 +1704,7 @@ function _archivePrdPostProcess({ planFile, archivePath, planPath, plansDir, _mu
|
|
|
1880
1704
|
|
|
1881
1705
|
// (a) Flip the archived FLAG + status + archivedAt. Phase 10 step 4.2b:
|
|
1882
1706
|
// archived-ness is the flag now (not the directory), so set `archived:true`
|
|
1883
|
-
// explicitly. The
|
|
1707
|
+
// explicitly. The path-shaped mutator routes PRDs into SQL.
|
|
1884
1708
|
// When archivePath === planPath (in-place archive) this flags the PRD where it
|
|
1885
1709
|
// sits; when they differ (.md-cascade / legacy move) it flags the moved copy.
|
|
1886
1710
|
try {
|
|
@@ -3244,7 +3068,8 @@ function serveFreshJson(req, res, opts) {
|
|
|
3244
3068
|
return;
|
|
3245
3069
|
}
|
|
3246
3070
|
const mtime = _maxInputMtimeMs(inputs);
|
|
3247
|
-
const
|
|
3071
|
+
const eventVersion = _getCurrentEventVersion();
|
|
3072
|
+
const etag = '"' + tag + '-' + mtime + '-' + eventVersion + '"';
|
|
3248
3073
|
res.setHeader('ETag', etag);
|
|
3249
3074
|
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
3250
3075
|
if (req && req.headers && req.headers['if-none-match'] === etag) {
|
|
@@ -6253,11 +6078,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
6253
6078
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
6254
6079
|
let prdPath = path.join(prdDir, body.file);
|
|
6255
6080
|
let fromArchive = false;
|
|
6256
|
-
if (!
|
|
6081
|
+
if (!statePathExists(prdPath)) {
|
|
6257
6082
|
prdPath = path.join(prdDir, 'archive', body.file);
|
|
6258
6083
|
fromArchive = true;
|
|
6259
6084
|
}
|
|
6260
|
-
if (!
|
|
6085
|
+
if (!statePathExists(prdPath)) return jsonReply(res, 404, { error: 'PRD not found' });
|
|
6261
6086
|
|
|
6262
6087
|
// If archived, temporarily restore to active so checkPlanCompletion can find it
|
|
6263
6088
|
const activePath = path.join(prdDir, body.file);
|
|
@@ -7217,14 +7042,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
7217
7042
|
const manualPrd = buildManualPrdItemPlan(body);
|
|
7218
7043
|
if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
|
|
7219
7044
|
|
|
7220
|
-
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
7221
|
-
|
|
7222
7045
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
7223
7046
|
const manualPrdPath = path.join(PRD_DIR, planFile);
|
|
7224
7047
|
safeWrite(manualPrdPath, manualPrd.plan);
|
|
7225
|
-
// Phase 10 step 2 — this create uses safeWrite (not mutateJsonFileLocked),
|
|
7226
|
-
// so mirror to SQL explicitly. Best-effort.
|
|
7227
|
-
prdStore.mirrorPrdToSql(manualPrdPath, manualPrd.plan);
|
|
7228
7048
|
invalidatePlansCache();
|
|
7229
7049
|
return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
|
|
7230
7050
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -7238,7 +7058,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7238
7058
|
// the file with JSON, destroying source plans (see /api/plans/approve trap).
|
|
7239
7059
|
if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
|
|
7240
7060
|
const planPath = resolvePlanPath(body.source);
|
|
7241
|
-
if (!
|
|
7061
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
|
|
7242
7062
|
// Pre-check: verify item exists before taking the lock
|
|
7243
7063
|
const preCheck = safeJsonObj(planPath);
|
|
7244
7064
|
const preItem = (preCheck.missing_features || []).find(f => f.id === body.itemId);
|
|
@@ -7295,7 +7115,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7295
7115
|
if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
|
|
7296
7116
|
if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
|
|
7297
7117
|
const planPath = resolvePlanPath(body.source);
|
|
7298
|
-
if (!
|
|
7118
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
|
|
7299
7119
|
let removed = false;
|
|
7300
7120
|
mutateJsonFileLocked(planPath, (plan) => {
|
|
7301
7121
|
if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = { missing_features: [] };
|
|
@@ -7896,6 +7716,46 @@ const server = http.createServer(async (req, res) => {
|
|
|
7896
7716
|
return jsonReply(res, 200, result);
|
|
7897
7717
|
}
|
|
7898
7718
|
|
|
7719
|
+
function handleMemoryRecordsSearch(req, res) {
|
|
7720
|
+
const url = new URL(req.url, 'http://localhost');
|
|
7721
|
+
const query = String(url.searchParams.get('q') || '').trim();
|
|
7722
|
+
if (!query) return jsonReply(res, 400, { error: 'q is required' });
|
|
7723
|
+
if (query.length > 500) return jsonReply(res, 400, { error: 'q must be 500 characters or fewer' });
|
|
7724
|
+
const limitRaw = Number.parseInt(url.searchParams.get('limit') || '20', 10);
|
|
7725
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(100, limitRaw)) : 20;
|
|
7726
|
+
const status = url.searchParams.get('status') || 'active';
|
|
7727
|
+
if (!memoryStore.STATUSES.has(status)) return jsonReply(res, 400, { error: 'invalid status' });
|
|
7728
|
+
try {
|
|
7729
|
+
const records = memoryStore.searchMemoryRecords(query, {
|
|
7730
|
+
allScopes: true,
|
|
7731
|
+
statuses: [status],
|
|
7732
|
+
limit,
|
|
7733
|
+
});
|
|
7734
|
+
const boundedRecords = records.map(record => ({
|
|
7735
|
+
...record,
|
|
7736
|
+
body: String(record.body || '').slice(0, 4000),
|
|
7737
|
+
bodyTruncated: String(record.body || '').length > 4000,
|
|
7738
|
+
}));
|
|
7739
|
+
return jsonReply(res, 200, { query, count: boundedRecords.length, records: boundedRecords });
|
|
7740
|
+
} catch (e) {
|
|
7741
|
+
return jsonReply(res, 400, { error: e.message });
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
|
|
7745
|
+
async function handleMemoryRecordAction(req, res, match) {
|
|
7746
|
+
const id = decodeURIComponent((match && match[1]) || '').trim();
|
|
7747
|
+
if (!id) return jsonReply(res, 400, { error: 'memory record id is required' });
|
|
7748
|
+
try {
|
|
7749
|
+
const body = await readBody(req);
|
|
7750
|
+
const record = memoryStore.updateMemoryRecordState(id, String(body.action || '').trim());
|
|
7751
|
+
if (!record) return jsonReply(res, 404, { error: 'memory record not found' });
|
|
7752
|
+
invalidateStatusCache();
|
|
7753
|
+
return jsonReply(res, 200, { ok: true, record });
|
|
7754
|
+
} catch (e) {
|
|
7755
|
+
return jsonReply(res, 400, { error: e.message });
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7899
7759
|
async function handleKnowledgeRead(req, res, match) {
|
|
7900
7760
|
const cat = match[1];
|
|
7901
7761
|
// P-bfa2b-kb-path-traversal — whitelist the category param BEFORE any
|
|
@@ -8000,9 +7860,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
8000
7860
|
{ dir: PRD_DIR, archived: false },
|
|
8001
7861
|
{ dir: path.join(PRD_DIR, 'archive'), archived: true },
|
|
8002
7862
|
];
|
|
8003
|
-
// Load work items to check for completed
|
|
8004
|
-
//
|
|
8005
|
-
const centralWi =
|
|
7863
|
+
// Load central work items from the SQL-backed store to check for completed
|
|
7864
|
+
// plan-to-prd conversions.
|
|
7865
|
+
const centralWi = require('./engine/work-items-store').readWorkItemsForScope('central');
|
|
8006
7866
|
const completedPrdFiles = new Set(
|
|
8007
7867
|
centralWi.filter(w => w.type === WORK_TYPE.PLAN_TO_PRD && DONE_STATUSES.has(w.status) && w.planFile)
|
|
8008
7868
|
.map(w => w.planFile)
|
|
@@ -8386,8 +8246,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
8386
8246
|
if (!body.file.endsWith('.md')) return jsonReply(res, 400, { error: 'only .md plans can be executed' });
|
|
8387
8247
|
shared.sanitizePath(body.file, PLANS_DIR);
|
|
8388
8248
|
const planPath = path.join(MINIONS_DIR, 'plans', body.file);
|
|
8389
|
-
if (!
|
|
8390
|
-
const planContent =
|
|
8249
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
|
|
8250
|
+
const planContent = safeRead(planPath);
|
|
8391
8251
|
const declaredProject = shared.extractPlanDeclaredProject(planContent);
|
|
8392
8252
|
|
|
8393
8253
|
const feedback = (body.feedback || '').toString().trim();
|
|
@@ -8444,7 +8304,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8444
8304
|
const body = await readBody(req);
|
|
8445
8305
|
if (!body.source) return jsonReply(res, 400, { error: 'source required' });
|
|
8446
8306
|
const planPath = resolvePlanPath(body.source);
|
|
8447
|
-
if (!
|
|
8307
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
|
|
8448
8308
|
const plan = safeJsonObj(planPath);
|
|
8449
8309
|
const planItems = plan.missing_features || [];
|
|
8450
8310
|
|
|
@@ -8540,7 +8400,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8540
8400
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
8541
8401
|
shared.sanitizePath(body.file, body.file.endsWith('.json') ? PRD_DIR : PLANS_DIR);
|
|
8542
8402
|
const planPath = resolvePlanPath(body.file);
|
|
8543
|
-
if (!
|
|
8403
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
|
|
8544
8404
|
// Read plan content before deleting — needed for worktree cleanup and source_plan
|
|
8545
8405
|
let planObj = null;
|
|
8546
8406
|
let prdSourcePlan = null;
|
|
@@ -8550,13 +8410,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
8550
8410
|
// Clean up worktrees before deleting work items (needs branch info from work items)
|
|
8551
8411
|
try {
|
|
8552
8412
|
const { cleanupPlanWorktrees } = require('./engine/lifecycle');
|
|
8553
|
-
cleanupPlanWorktrees(body.file, planObj || {}, PROJECTS, getConfig());
|
|
8413
|
+
cleanupPlanWorktrees(body.file, planObj || {}, PROJECTS, queries.getConfig());
|
|
8554
8414
|
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
8555
8415
|
safeUnlink(planPath);
|
|
8556
8416
|
// Phase 10 step 2 — a deleted PRD must also leave SQL, or the read-flip
|
|
8557
8417
|
// (step 3) would resurrect it from a stale mirror row. Best-effort; no-op
|
|
8558
8418
|
// for non-PRD paths (parsePrdPath returns null for plan .md deletes).
|
|
8559
|
-
if (body.file.endsWith('.json')) prdStore.
|
|
8419
|
+
if (body.file.endsWith('.json')) prdStore.deletePrd(planPath);
|
|
8560
8420
|
// Neutralize the `.backup` sidecar so `safeJson` auto-restore can't
|
|
8561
8421
|
// RESURRECT the PRD we just deleted (the live file is gone, but a stray
|
|
8562
8422
|
// `prd/<plan>.json.backup` would be auto-restored on the next safeJson read
|
|
@@ -8626,7 +8486,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8626
8486
|
const isPrd = body.file.endsWith('.json');
|
|
8627
8487
|
shared.sanitizePath(body.file, isPrd ? PRD_DIR : PLANS_DIR);
|
|
8628
8488
|
const planPath = resolvePlanPath(body.file);
|
|
8629
|
-
if (!
|
|
8489
|
+
if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
|
|
8630
8490
|
|
|
8631
8491
|
// Phase 10 step 4.2b: a PRD is archived IN PLACE (flag flip via
|
|
8632
8492
|
// _archivePrdPostProcess — the json stays in prd/, no move, so the
|
|
@@ -8669,19 +8529,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8669
8529
|
// shipped for the PRD branch in W-mqa13ulk0002def5 (PR #3222) — the
|
|
8670
8530
|
// helper's source-plan move is a safe no-op here because the outer
|
|
8671
8531
|
// handler already renamed body.file into plans/archive/.
|
|
8672
|
-
|
|
8673
|
-
try {
|
|
8674
|
-
prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
|
|
8675
|
-
} catch (e) {
|
|
8676
|
-
// ENOENT on prd/ is expected for projects without a PRD dir yet —
|
|
8677
|
-
// don't surface as a user-visible warning. Other errors (EACCES,
|
|
8678
|
-
// EIO) get logged + warned.
|
|
8679
|
-
if (e.code !== 'ENOENT') {
|
|
8680
|
-
const warning = `Archive could not enumerate ${PRD_DIR}: ${e.message}`;
|
|
8681
|
-
archiveWarnings.push(warning);
|
|
8682
|
-
console.warn(warning);
|
|
8683
|
-
}
|
|
8684
|
-
}
|
|
8532
|
+
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
8685
8533
|
for (const prdFile of prdFiles) {
|
|
8686
8534
|
const prdLivePath = path.join(PRD_DIR, prdFile);
|
|
8687
8535
|
let prd = null;
|
|
@@ -8736,7 +8584,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8736
8584
|
|
|
8737
8585
|
try {
|
|
8738
8586
|
const { cleanupPlanWorktrees } = require('./engine/lifecycle');
|
|
8739
|
-
cleanupPlanWorktrees(body.file, plan, PROJECTS, getConfig());
|
|
8587
|
+
cleanupPlanWorktrees(body.file, plan, PROJECTS, queries.getConfig());
|
|
8740
8588
|
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
8741
8589
|
|
|
8742
8590
|
invalidateStatusCache();
|
|
@@ -8763,12 +8611,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
8763
8611
|
// (physically in <dir>/archive/) or in-place (in <dir>/ with the archived
|
|
8764
8612
|
// flag set). Restore from whichever it is.
|
|
8765
8613
|
let liveDest;
|
|
8766
|
-
|
|
8614
|
+
// Existence probes only — NEVER used as the write payload. The actual
|
|
8615
|
+
// restore below (prdStore.restorePrdFromArchive) re-reads the archived
|
|
8616
|
+
// row fresh inside its own transaction immediately before writing, so a
|
|
8617
|
+
// concurrent engine write to that row between this probe and the write
|
|
8618
|
+
// can't be clobbered by a stale in-memory snapshot (W-mrenu075000a94c9).
|
|
8619
|
+
const archivedPrdExists = isJson ? prdStore.readPrd(archivePath) != null : false;
|
|
8620
|
+
const activePrdData = isJson ? prdStore.readPrd(livePath) : null;
|
|
8621
|
+
if (isJson && archivedPrdExists) {
|
|
8622
|
+
const restored = prdStore.restorePrdFromArchive(body.file);
|
|
8623
|
+
if (!restored) return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
8624
|
+
liveDest = livePath;
|
|
8625
|
+
} else if (fs.existsSync(archivePath)) {
|
|
8767
8626
|
// DATA-LOSS GUARD: restoring must not clobber a LIVE plan/PRD of the same
|
|
8768
8627
|
// basename (renameSync overwrites). moveFileNoClobber bumps the restored
|
|
8769
8628
|
// name instead so the existing live file survives.
|
|
8770
8629
|
liveDest = shared.moveFileNoClobber(archivePath, targetDir, body.file);
|
|
8771
|
-
} else if (isJson &&
|
|
8630
|
+
} else if (isJson && activePrdData && shared.isPrdArchived(activePrdData)) {
|
|
8772
8631
|
liveDest = livePath; // in-place flag-archived — already in prd/, just clear the flag below
|
|
8773
8632
|
} else {
|
|
8774
8633
|
return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
@@ -11360,6 +11219,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11360
11219
|
// 1 floor (sequential fallback) and 20 ceiling (above this the LLM
|
|
11361
11220
|
// provider's per-second rate limits dominate; throughput gains taper).
|
|
11362
11221
|
preDispatchEvalConcurrency: [1, 20],
|
|
11222
|
+
memoryRetrievalTopK: [1, 30],
|
|
11223
|
+
memoryRetrievalMaxBytes: [1024, 65536],
|
|
11224
|
+
memoryRetrievalCandidateLimit: [5, 200],
|
|
11363
11225
|
};
|
|
11364
11226
|
for (const [key, [min, max]] of Object.entries(numericFields)) {
|
|
11365
11227
|
if (e[key] !== undefined) {
|
|
@@ -13566,10 +13428,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13566
13428
|
},
|
|
13567
13429
|
});
|
|
13568
13430
|
}},
|
|
13569
|
-
{ method: 'GET', path: '/api/schedules', desc: 'Schedule definitions merged with schedule-
|
|
13431
|
+
{ method: 'GET', path: '/api/schedules', desc: 'Schedule definitions merged with SQL schedule-run state (_lastRun/_lastResult/_lastCompletedAt)', handler: (req, res) => {
|
|
13570
13432
|
return serveFreshJson(req, res, {
|
|
13571
13433
|
tag: 'schedules',
|
|
13572
|
-
inputs: [CONFIG_PATH
|
|
13434
|
+
inputs: [CONFIG_PATH],
|
|
13573
13435
|
builder: () => {
|
|
13574
13436
|
// Read config.json directly via queries.getConfig() rather than
|
|
13575
13437
|
// calling reloadConfig() — the latter cascades into
|
|
@@ -13581,7 +13443,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13581
13443
|
// (Round-2 review finding #3.)
|
|
13582
13444
|
const cfg = queries.getConfig() || {};
|
|
13583
13445
|
const scheds = cfg.schedules || [];
|
|
13584
|
-
const runs =
|
|
13446
|
+
const runs = require('./engine/small-state-store').readScheduleRuns();
|
|
13585
13447
|
return scheds.map(s => {
|
|
13586
13448
|
const runEntry = runs[s.id];
|
|
13587
13449
|
const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
|
|
@@ -13598,12 +13460,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13598
13460
|
},
|
|
13599
13461
|
});
|
|
13600
13462
|
}},
|
|
13601
|
-
{ method: 'GET', path: '/api/pipelines', desc: 'Pipeline definitions merged with last-5
|
|
13463
|
+
{ method: 'GET', path: '/api/pipelines', desc: 'Pipeline definitions merged with last-5 SQL-backed runs', handler: (req, res) => {
|
|
13602
13464
|
return serveFreshJson(req, res, {
|
|
13603
13465
|
tag: 'pipelines',
|
|
13604
13466
|
inputs: [
|
|
13605
13467
|
path.join(MINIONS_DIR, 'pipelines'),
|
|
13606
|
-
path.join(ENGINE_DIR, 'pipeline-runs.json'),
|
|
13607
13468
|
],
|
|
13608
13469
|
builder: () => {
|
|
13609
13470
|
try {
|
|
@@ -13618,14 +13479,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13618
13479
|
{ method: 'GET', path: '/api/qa-runs-summary', desc: 'Sidebar-counter summary {total, sig} for QA runs (sidebar activity-dot signal)', handler: (req, res) => {
|
|
13619
13480
|
return serveFreshJson(req, res, {
|
|
13620
13481
|
tag: 'qa-runs-summary',
|
|
13621
|
-
inputs: [
|
|
13482
|
+
inputs: [],
|
|
13622
13483
|
builder: () => qaRunsMod.summarizeRunsForStatus(),
|
|
13623
13484
|
});
|
|
13624
13485
|
}},
|
|
13625
13486
|
{ method: 'GET', path: '/api/qa-sessions-summary', desc: 'Sidebar-counter summary {active, sig} for non-terminal QA sessions (drives the QA nav badge — issue #717)', handler: (req, res) => {
|
|
13626
13487
|
return serveFreshJson(req, res, {
|
|
13627
13488
|
tag: 'qa-sessions-summary',
|
|
13628
|
-
inputs: [
|
|
13489
|
+
inputs: [],
|
|
13629
13490
|
builder: () => qaSessionsMod.summarizeActiveSessionsForStatus(),
|
|
13630
13491
|
});
|
|
13631
13492
|
}},
|
|
@@ -13912,7 +13773,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13912
13773
|
if (token) ghOpts.env = { ...process.env, GH_TOKEN: token };
|
|
13913
13774
|
const result = await shared.shellSafeGh(['api', `repos/${shared.validateGhSlug(slug)}/pulls/${String(prNum)}`], ghOpts);
|
|
13914
13775
|
const d = JSON.parse(result);
|
|
13915
|
-
prData = { title: d.title, description: d.body, branch: d.head?.ref, author: d.user?.login };
|
|
13776
|
+
prData = { title: d.title, description: d.body, branch: d.head?.ref, author: d.user?.login, created: d.created_at };
|
|
13916
13777
|
} else if (adoTarget && !initialPrData) {
|
|
13917
13778
|
try {
|
|
13918
13779
|
prData = await ado.fetchAdoPrMetadata(adoTarget.prNum, adoTarget.adoOrg, adoTarget.adoProj, adoTarget.adoRepo);
|
|
@@ -13939,6 +13800,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13939
13800
|
}
|
|
13940
13801
|
}
|
|
13941
13802
|
if (pr.agent === 'human' && prData.author) pr.agent = prData.author;
|
|
13803
|
+
// W-mrezh0yb0007f733 — for GitHub (and ADO metadata-fetch-retry)
|
|
13804
|
+
// links, `created` was seeded with link-time now(); adopt the
|
|
13805
|
+
// platform's real creation timestamp now that enrichment fetched it.
|
|
13806
|
+
if (prData.created && shared.shouldAdoptPlatformCreated(pr.created, prData.created)) {
|
|
13807
|
+
pr.created = prData.created;
|
|
13808
|
+
}
|
|
13942
13809
|
return prs;
|
|
13943
13810
|
}, { defaultValue: [] });
|
|
13944
13811
|
invalidateStatusCache();
|
|
@@ -14560,7 +14427,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14560
14427
|
} },
|
|
14561
14428
|
|
|
14562
14429
|
// Knowledge base
|
|
14430
|
+
{ method: 'GET', path: '/assets/memory-search.js', desc: 'Serve the lazy-loaded structured memory search UI', handler: (req, res) => {
|
|
14431
|
+
const content = safeRead(path.join(MINIONS_DIR, 'dashboard', 'js', 'memory-search.js'));
|
|
14432
|
+
if (content == null) { res.statusCode = 404; return res.end('not found'); }
|
|
14433
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
14434
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
14435
|
+
res.end(content);
|
|
14436
|
+
} },
|
|
14563
14437
|
{ method: 'GET', path: '/api/knowledge', desc: 'List all knowledge base entries grouped by category', handler: handleKnowledgeList },
|
|
14438
|
+
{ method: 'GET', path: /^\/api\/memory-records\/search(?:\?.*)?$/, template: '/api/memory-records/search', desc: 'Search structured memory records with provenance and lifecycle status', params: 'q, status?, limit?', handler: handleMemoryRecordsSearch },
|
|
14439
|
+
{ method: 'POST', path: /^\/api\/memory-records\/([^/?]+)\/action$/, template: '/api/memory-records/<id>/action', desc: 'Pin, retract, or restore a structured memory record', params: 'action (pin|retract|restore)', handler: handleMemoryRecordAction },
|
|
14564
14440
|
{ method: 'POST', path: '/api/knowledge', desc: 'Create a knowledge base entry', params: 'category, title, content', handler: async (req, res) => {
|
|
14565
14441
|
const body = await readBody(req);
|
|
14566
14442
|
const { category, title, content } = body;
|
|
@@ -14572,7 +14448,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14572
14448
|
const dir = path.dirname(filePath);
|
|
14573
14449
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
14574
14450
|
const header = '# ' + title + '\n\n*Created by human teammate on ' + new Date().toISOString().slice(0, 10) + '*\n\n';
|
|
14575
|
-
|
|
14451
|
+
const fullContent = header + content;
|
|
14452
|
+
safeWrite(filePath, fullContent);
|
|
14453
|
+
memoryStore.upsertMemoryRecord({
|
|
14454
|
+
memoryType: 'semantic',
|
|
14455
|
+
scopeType: 'global',
|
|
14456
|
+
title,
|
|
14457
|
+
body: fullContent,
|
|
14458
|
+
tags: [category],
|
|
14459
|
+
sourceType: 'knowledge-base',
|
|
14460
|
+
sourcePath: path.relative(MINIONS_DIR, filePath).replace(/\\/g, '/'),
|
|
14461
|
+
sourceRef: path.relative(MINIONS_DIR, filePath).replace(/\\/g, '/'),
|
|
14462
|
+
trust: 'human',
|
|
14463
|
+
importance: 0.8,
|
|
14464
|
+
confidence: 1,
|
|
14465
|
+
});
|
|
14576
14466
|
queries.invalidateKnowledgeBaseCache();
|
|
14577
14467
|
invalidateStatusCache();
|
|
14578
14468
|
recordCcTurnIfPresent(req, { kind: 'knowledge', title, path: filePath });
|
|
@@ -14596,8 +14486,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14596
14486
|
// Skills
|
|
14597
14487
|
{ method: 'GET', path: '/api/skill', desc: 'Read a skill file', params: 'file, source?, dir?', handler: handleSkillRead },
|
|
14598
14488
|
|
|
14599
|
-
//
|
|
14600
|
-
{ method: 'GET', path: '/api/harness/diagnostics', desc: '
|
|
14489
|
+
// Runtime harness inventory
|
|
14490
|
+
{ method: 'GET', path: '/api/harness/diagnostics', desc: 'Read-only inventory of runtime-declared harness locations and explicit orchestration policies', handler: handleHarnessDiagnostics },
|
|
14601
14491
|
|
|
14602
14492
|
// Projects
|
|
14603
14493
|
{ method: 'POST', path: '/api/projects/browse', desc: 'Open folder picker dialog, return selected path', handler: handleProjectsBrowse },
|