@yemi33/minions 0.1.2379 → 0.1.2381
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 +19 -9
- package/dashboard/js/refresh.js +3 -4
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +435 -768
- package/docs/architecture-review-2026-07-09.md +2 -4
- package/docs/auto-discovery.md +36 -49
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/docs/branch-derivation.md +2 -2
- package/docs/command-center.md +1 -1
- package/docs/completion-reports.md +14 -4
- package/docs/constellation-bridge.md +59 -10
- package/docs/constellation-style-telemetry.md +6 -6
- package/docs/cooldown-merge-semantics.md +4 -0
- package/docs/copilot-cli-schema.md +3 -3
- package/docs/cross-repo-plans.md +17 -17
- package/docs/deprecated.json +2 -2
- package/docs/design-state-storage.md +1 -1
- package/docs/documentation-audit-2026-07-09.md +2 -2
- package/docs/engine-restart.md +20 -8
- package/docs/harness-mode.md +1 -1
- package/docs/live-checkout-mode.md +45 -26
- package/docs/managed-spawn.md +4 -4
- package/docs/onboarding.md +1 -2
- package/docs/pr-comment-followup.md +3 -3
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
- package/docs/qa-runbook-lifecycle.md +4 -4
- package/docs/qa-runbooks.md +2 -2
- package/docs/rfc-completion-json.md +4 -1
- package/docs/runtime-adapters.md +1 -1
- package/docs/self-improvement.md +4 -5
- package/docs/shared-lifecycle-module-map.md +3 -1
- package/docs/slim-ux/architecture-suggestions.md +5 -6
- package/docs/slim-ux/concepts.md +23 -25
- package/docs/watches.md +7 -7
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +1 -1
- package/engine/abandoned-pr-reconciliation.js +4 -5
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +58 -1
- package/engine/bridge.js +260 -5
- package/engine/cleanup.js +48 -131
- package/engine/cli.js +125 -83
- package/engine/cooldown.js +9 -16
- package/engine/db/index.js +22 -9
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/020-qa-session-scopes.js +23 -0
- package/engine/db/migrations/021-archived-work-items.js +72 -0
- package/engine/db/migrations/022-global-cc-session.js +31 -0
- package/engine/db/migrations/023-engine-state.js +30 -0
- package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
- package/engine/dispatch-store.js +2 -7
- package/engine/dispatch.js +39 -44
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +279 -354
- package/engine/live-checkout.js +193 -149
- package/engine/llm.js +1 -1
- package/engine/logs-store.js +2 -2
- package/engine/managed-spawn.js +2 -23
- package/engine/meeting.js +6 -6
- package/engine/metrics-store.js +2 -2
- package/engine/note-link-backfill.js +6 -11
- package/engine/pipeline.js +18 -36
- package/engine/playbook.js +13 -16
- package/engine/prd-store.js +73 -54
- package/engine/preflight.js +2 -5
- package/engine/projects.js +15 -62
- package/engine/pull-requests-store.js +0 -17
- package/engine/qa-runbooks.js +2 -2
- package/engine/qa-runs.js +1 -8
- package/engine/qa-sessions.js +41 -64
- package/engine/queries.js +120 -219
- package/engine/routing.js +4 -6
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +268 -699
- package/engine/small-state-store.js +89 -10
- package/engine/state-operations.js +16 -4
- package/engine/stdio-timestamps.js +1 -1
- package/engine/timeout.js +5 -12
- package/engine/watch-actions.js +20 -22
- package/engine/watches-store.js +1 -1
- package/engine/watches.js +6 -10
- package/engine/work-item-validation.js +52 -0
- package/engine/work-items-store.js +127 -29
- package/engine/worktree-gc.js +2 -2
- package/engine/worktree-pool.js +8 -18
- package/engine.js +197 -358
- package/minions.js +2 -2
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
- package/playbooks/shared-rules.md +3 -3
- package/playbooks/templates/followup-dispatch.md +1 -1
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +9 -9
package/engine/queries.js
CHANGED
|
@@ -12,13 +12,13 @@ const shared = require('./shared');
|
|
|
12
12
|
const steering = require('./steering');
|
|
13
13
|
|
|
14
14
|
const { safeRead, safeReadDir, safeJson, safeJsonObj, safeWrite, getProjects, mutateJsonFileLocked,
|
|
15
|
-
|
|
15
|
+
parseSkillFrontmatter, KB_CATEGORIES,
|
|
16
16
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS } = shared;
|
|
17
17
|
|
|
18
18
|
// ─── Defensive PR enrichment cache (W-mq5uzmc6001d708f) ──────────────────────
|
|
19
19
|
//
|
|
20
20
|
// buildPrdProgress runs on every /api/status poll. When a work item carries
|
|
21
|
-
// `_pr` but no canonical row exists
|
|
21
|
+
// `_pr` but no canonical tracked row exists, the synthesis
|
|
22
22
|
// fallback below used to default to PR_STATUS.ACTIVE forever (blue ○). Now
|
|
23
23
|
// we cache the resolved status per canonical id and kick off a best-effort
|
|
24
24
|
// fire-and-forget enrollment so the next poll cycle sees the real status.
|
|
@@ -74,14 +74,14 @@ function _resetPrEnrichmentCacheForTest() {
|
|
|
74
74
|
|
|
75
75
|
// W-mrcs8yqk001o6893 — shared enrichment-fallback resolver. Both the prLinks
|
|
76
76
|
// loop and the wi._pr fallback loop in getPrdInfo() need the SAME defensive
|
|
77
|
-
// behavior when the canonical PR id has no live record
|
|
77
|
+
// behavior when the canonical PR id has no live tracked record
|
|
78
78
|
// (e.g. it was merged and then swept by the merged-PR cleanup routine): check
|
|
79
79
|
// the enrichment cache first, else synthesize PR_STATUS.ACTIVE as a
|
|
80
80
|
// placeholder and kick off a fire-and-forget enrollment so the next poll
|
|
81
81
|
// (within PR_ENRICHMENT_TTL_MS) resolves the real status. Previously only the
|
|
82
82
|
// wi._pr loop did this (W-mq5uzmc6001d708f); the prLinks loop hardcoded
|
|
83
83
|
// `pr?.status || PR_STATUS.ACTIVE` with no self-heal, so PRs reached only via
|
|
84
|
-
// pr_links stayed 'active' forever once removed from
|
|
84
|
+
// pr_links stayed 'active' forever once removed from tracked PR state.
|
|
85
85
|
function _resolvePrStatusWithEnrichment(pr, canonicalPrId, project, itemId) {
|
|
86
86
|
if (pr?.status) return pr.status;
|
|
87
87
|
if (!canonicalPrId) return PR_STATUS.ACTIVE;
|
|
@@ -162,8 +162,6 @@ const ARCHIVE_DIR = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
|
162
162
|
|
|
163
163
|
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
164
164
|
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
165
|
-
const DISPATCH_PATH = path.join(ENGINE_DIR, 'dispatch.json');
|
|
166
|
-
const LOG_PATH = path.join(ENGINE_DIR, 'log.json');
|
|
167
165
|
const NOTES_PATH = path.join(MINIONS_DIR, 'notes.md');
|
|
168
166
|
|
|
169
167
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -245,7 +243,7 @@ function getDispatch() {
|
|
|
245
243
|
// status derivation, work-item PR enrichment, etc.) but only changes on
|
|
246
244
|
// mutateDispatch. SQL reads are sub-millisecond, so the cache is now
|
|
247
245
|
// mostly a per-request memo rather than the disk-read amortizer it used
|
|
248
|
-
// to be
|
|
246
|
+
// to be with legacy megabyte-scale dispatch files.
|
|
249
247
|
const now = Date.now();
|
|
250
248
|
if (_dispatchCache && (now - _dispatchCacheAt) < 2000) return _dispatchCache;
|
|
251
249
|
// SQL is the canonical (and only) dispatch store after Phase 9.
|
|
@@ -438,7 +436,7 @@ function getMetrics() {
|
|
|
438
436
|
};
|
|
439
437
|
}
|
|
440
438
|
|
|
441
|
-
// Enrich agent PR counts from
|
|
439
|
+
// Enrich agent PR counts from tracked PR state.
|
|
442
440
|
const allPrs = getPullRequests();
|
|
443
441
|
const prCountByAgent = {};
|
|
444
442
|
const prApprovedByAgent = {};
|
|
@@ -449,7 +447,7 @@ function getMetrics() {
|
|
|
449
447
|
prCountByAgent[agent] = (prCountByAgent[agent] || 0) + 1;
|
|
450
448
|
if (pr.reviewStatus === shared.REVIEW_STATUS.APPROVED || pr.status === shared.PR_STATUS.MERGED) prApprovedByAgent[agent] = (prApprovedByAgent[agent] || 0) + 1;
|
|
451
449
|
// BUG-H13 / P-h13-rejected: this used to compare to the literal 'rejected',
|
|
452
|
-
// which never appears in
|
|
450
|
+
// which never appears in tracked PR state — the canonical enum value is
|
|
453
451
|
// REVIEW_STATUS.CHANGES_REQUESTED ('changes-requested'). The miscompare
|
|
454
452
|
// permanently zeroed prRejectedByAgent, and the snapshot then clobbered
|
|
455
453
|
// shared.trackReviewMetric()'s cumulative counter at the apply step below.
|
|
@@ -536,6 +534,37 @@ function _wiIdFromNoteFrontmatter(fm) {
|
|
|
536
534
|
return fm.work_item || fm.wi || fm.sourceItem || null;
|
|
537
535
|
}
|
|
538
536
|
|
|
537
|
+
const WORK_ITEM_KEY_SEPARATOR = '\u0000';
|
|
538
|
+
|
|
539
|
+
function _workItemIdentityKey(item, fallbackScope = null) {
|
|
540
|
+
if (!item?.id) return null;
|
|
541
|
+
const scope = item._source || fallbackScope || item.project || 'central';
|
|
542
|
+
return `${scope}${WORK_ITEM_KEY_SEPARATOR}${item.id}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function _dispatchWorkItemIdentityKey(entry) {
|
|
546
|
+
const item = entry?.meta?.item;
|
|
547
|
+
if (!item?.id) return null;
|
|
548
|
+
const fallbackScope = entry.meta.source === 'central-work-item'
|
|
549
|
+
|| entry.meta.source === 'central-work-item-fanout'
|
|
550
|
+
? 'central'
|
|
551
|
+
: (entry.meta.project?.name || item.project || 'central');
|
|
552
|
+
return _workItemIdentityKey(item, fallbackScope);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function _noteIdentityKey(fm, dispatchIdentityById) {
|
|
556
|
+
const wiId = _wiIdFromNoteFrontmatter(fm);
|
|
557
|
+
if (!wiId) return null;
|
|
558
|
+
const explicitScope = fm.work_item_scope || fm.scope;
|
|
559
|
+
if (explicitScope) return `${explicitScope}${WORK_ITEM_KEY_SEPARATOR}${wiId}`;
|
|
560
|
+
if (fm.dispatchId && dispatchIdentityById.has(fm.dispatchId)) {
|
|
561
|
+
return dispatchIdentityById.get(fm.dispatchId);
|
|
562
|
+
}
|
|
563
|
+
const inferredScope = fm.project || fm.sourceProject;
|
|
564
|
+
if (inferredScope) return `${inferredScope}${WORK_ITEM_KEY_SEPARATOR}${wiId}`;
|
|
565
|
+
return wiId;
|
|
566
|
+
}
|
|
567
|
+
|
|
539
568
|
// Archive notes-by-WI cache. The archive dir accumulates thousands of
|
|
540
569
|
// immutable note files (2000+ and growing); reading + frontmatter-parsing all
|
|
541
570
|
// of them was the single dominant SYNCHRONOUS cost inside getWorkItems()
|
|
@@ -558,7 +587,7 @@ let _archiveNotesByWiCacheMtime = null;
|
|
|
558
587
|
// full multi-thousand-file re-read on a fixed cadence when nothing changed.
|
|
559
588
|
const ARCHIVE_NOTES_BY_WI_TTL = 300000; // 5 min
|
|
560
589
|
|
|
561
|
-
function
|
|
590
|
+
function _buildArchiveNoteRefs() {
|
|
562
591
|
let dirMtime = null;
|
|
563
592
|
try { dirMtime = fs.statSync(ARCHIVE_DIR).mtimeMs; } catch { dirMtime = null; }
|
|
564
593
|
const now = Date.now();
|
|
@@ -567,14 +596,13 @@ function _buildArchiveNotesByWiMap() {
|
|
|
567
596
|
&& _archiveNotesByWiCacheMtime === dirMtime) {
|
|
568
597
|
return _archiveNotesByWiCache;
|
|
569
598
|
}
|
|
570
|
-
const out =
|
|
599
|
+
const out = [];
|
|
571
600
|
for (const f of safeReadDir(ARCHIVE_DIR)) {
|
|
572
601
|
if (!f.endsWith('.md')) continue;
|
|
573
602
|
const fm = _parseNoteFrontmatter(safeRead(path.join(ARCHIVE_DIR, f)));
|
|
574
603
|
const wiId = _wiIdFromNoteFrontmatter(fm);
|
|
575
604
|
if (!wiId) continue;
|
|
576
|
-
|
|
577
|
-
out[wiId].push('archive:' + f);
|
|
605
|
+
out.push({ fm, token: 'archive:' + f });
|
|
578
606
|
}
|
|
579
607
|
_archiveNotesByWiCache = out;
|
|
580
608
|
_archiveNotesByWiCacheAt = now;
|
|
@@ -586,26 +614,30 @@ function _buildArchiveNotesByWiMap() {
|
|
|
586
614
|
// Archive entries are prefixed with `archive:` so the dashboard renderer can
|
|
587
615
|
// tell them apart (matches the convention already used by _artifacts.notes).
|
|
588
616
|
// Pure-function helper exposed for testing.
|
|
589
|
-
function _buildNotesByWiMap() {
|
|
617
|
+
function _buildNotesByWiMap(dispatch) {
|
|
590
618
|
const out = Object.create(null);
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
619
|
+
const dispatchIdentityById = new Map();
|
|
620
|
+
for (const section of ['pending', 'active', 'completed', 'review']) {
|
|
621
|
+
for (const entry of (dispatch?.[section] || [])) {
|
|
622
|
+
const key = _dispatchWorkItemIdentityKey(entry);
|
|
623
|
+
if (entry?.id && key) dispatchIdentityById.set(entry.id, key);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const addNote = (fm, token) => {
|
|
627
|
+
const key = _noteIdentityKey(fm, dispatchIdentityById);
|
|
628
|
+
if (!key || !token) return;
|
|
629
|
+
if (!out[key]) out[key] = [];
|
|
630
|
+
if (!out[key].includes(token)) out[key].push(token);
|
|
595
631
|
};
|
|
596
632
|
// Inbox: tiny and churns constantly — scan fresh every call.
|
|
597
633
|
for (const f of safeReadDir(INBOX_DIR)) {
|
|
598
634
|
if (!f.endsWith('.md')) continue;
|
|
599
635
|
const fm = _parseNoteFrontmatter(safeRead(path.join(INBOX_DIR, f)));
|
|
600
|
-
|
|
601
|
-
if (wiId) addNote(wiId, f);
|
|
636
|
+
addNote(fm, f);
|
|
602
637
|
}
|
|
603
638
|
// Archive: large + immutable — served from the mtime-gated cache so we don't
|
|
604
639
|
// re-read thousands of files on every getWorkItems() rebuild.
|
|
605
|
-
const
|
|
606
|
-
for (const wiId in archiveMap) {
|
|
607
|
-
for (const token of archiveMap[wiId]) addNote(wiId, token);
|
|
608
|
-
}
|
|
640
|
+
for (const ref of _buildArchiveNoteRefs()) addNote(ref.fm, ref.token);
|
|
609
641
|
return out;
|
|
610
642
|
}
|
|
611
643
|
|
|
@@ -613,10 +645,16 @@ function _buildNotesByWiMap() {
|
|
|
613
645
|
// `archive:<filename>`) whose frontmatter cites this WI id. Returns [] when
|
|
614
646
|
// nothing matches. Scans inbox + archive each call; for the bulk WI list
|
|
615
647
|
// payload getWorkItems() pre-builds the map once per cache cycle.
|
|
616
|
-
function notesForWorkItem(wiId) {
|
|
648
|
+
function notesForWorkItem(wiId, scope) {
|
|
617
649
|
if (!wiId) return [];
|
|
618
650
|
const map = _buildNotesByWiMap();
|
|
619
|
-
return map[wiId] || [];
|
|
651
|
+
if (scope) return map[`${scope}${WORK_ITEM_KEY_SEPARATOR}${wiId}`] || map[wiId] || [];
|
|
652
|
+
const matches = [...(map[wiId] || [])];
|
|
653
|
+
const suffix = `${WORK_ITEM_KEY_SEPARATOR}${wiId}`;
|
|
654
|
+
for (const key of Object.keys(map)) {
|
|
655
|
+
if (key.endsWith(suffix)) matches.push(...map[key]);
|
|
656
|
+
}
|
|
657
|
+
return [...new Set(matches)];
|
|
620
658
|
}
|
|
621
659
|
|
|
622
660
|
function getInbox() {
|
|
@@ -636,7 +674,7 @@ function getInbox() {
|
|
|
636
674
|
|
|
637
675
|
// ── Agents ──────────────────────────────────────────────────────────────────
|
|
638
676
|
|
|
639
|
-
// Agent status is
|
|
677
|
+
// Agent status is derived from dispatch state.
|
|
640
678
|
// dispatch.active entry for this agent → working
|
|
641
679
|
// dispatch.completed (most recent) → done/error
|
|
642
680
|
// neither → idle
|
|
@@ -684,7 +722,7 @@ function getAgentStatus(agentId) {
|
|
|
684
722
|
}
|
|
685
723
|
|
|
686
724
|
// Fallback: derive active state from work-item markers.
|
|
687
|
-
// This protects UI status when dispatch
|
|
725
|
+
// This protects UI status when dispatch and work-item state briefly desync.
|
|
688
726
|
// Guard: only trust dispatched state within 2x stale-orphan timeout to prevent stale
|
|
689
727
|
// dispatched items from permanently showing an agent as working after a dead process.
|
|
690
728
|
try {
|
|
@@ -720,7 +758,7 @@ function getAgentStatus(agentId) {
|
|
|
720
758
|
return { status: 'idle', task: null, started_at: null, completed_at: null };
|
|
721
759
|
}
|
|
722
760
|
|
|
723
|
-
// setAgentStatus removed — agent status is derived from dispatch.
|
|
761
|
+
// setAgentStatus removed — agent status is derived from dispatch state.
|
|
724
762
|
// Status.json files no longer exist.
|
|
725
763
|
|
|
726
764
|
function getAgents(config) {
|
|
@@ -755,7 +793,7 @@ function getAgents(config) {
|
|
|
755
793
|
const inboxFiles = allInboxFiles.filter(f => f.includes(a.id));
|
|
756
794
|
let steeringInboxFiles = [];
|
|
757
795
|
try { steeringInboxFiles = steering.listUnreadSteeringMessages(a.id); } catch { steeringInboxFiles = []; }
|
|
758
|
-
const s = getAgentStatus(a.id); // derives from dispatch
|
|
796
|
+
const s = getAgentStatus(a.id); // derives from dispatch state
|
|
759
797
|
|
|
760
798
|
let lastAction = 'Waiting for assignment';
|
|
761
799
|
let lastActionPrefix = null;
|
|
@@ -812,7 +850,7 @@ function getAgentDetail(id) {
|
|
|
812
850
|
let outputLog = safeRead(path.join(agentDir, 'output.log')) || '';
|
|
813
851
|
if (outputLog.length > 50000) outputLog = '…(truncated — showing last 50KB)\n\n' + outputLog.slice(-50000);
|
|
814
852
|
|
|
815
|
-
const statusData = getAgentStatus(id); // derives from dispatch
|
|
853
|
+
const statusData = getAgentStatus(id); // derives from dispatch state
|
|
816
854
|
|
|
817
855
|
const inboxContents = safeReadDir(INBOX_DIR)
|
|
818
856
|
.filter(f => f.includes(id))
|
|
@@ -846,7 +884,7 @@ function getAgentDetail(id) {
|
|
|
846
884
|
function getPrs(project) {
|
|
847
885
|
if (project) {
|
|
848
886
|
const store = require('./pull-requests-store');
|
|
849
|
-
const prs = store.readPullRequestsForScope(
|
|
887
|
+
const prs = store.readPullRequestsForScope(project.name) || [];
|
|
850
888
|
shared.normalizePrRecords(prs, project);
|
|
851
889
|
return prs;
|
|
852
890
|
}
|
|
@@ -1796,22 +1834,14 @@ function getKnowledgeBaseIndex() {
|
|
|
1796
1834
|
// (W-mpodww9h000b460a). The dashboard's `_buildStatusFastState` calls it
|
|
1797
1835
|
// directly for the `workItems` slice, AND every idle agent's
|
|
1798
1836
|
// `getAgentStatus()` fallback re-reads it to derive a work-item-marker
|
|
1799
|
-
// "working" state when dispatch
|
|
1800
|
-
//
|
|
1801
|
-
// work-items.json, the pre-cache cost was ~300 ms per fast-state rebuild —
|
|
1837
|
+
// "working" state when dispatch state briefly desyncs from work-item state.
|
|
1838
|
+
// With 5 agents (3 idle), the pre-cache cost was ~300 ms per rebuild —
|
|
1802
1839
|
// the single biggest hot-path offender. Mirrors `_prsCache` / `_dispatchCache`
|
|
1803
1840
|
// (1 s TTL eliminates intra-request duplication without masking real
|
|
1804
1841
|
// updates from the next 4 s SPA poll cycle).
|
|
1805
1842
|
//
|
|
1806
|
-
// mtime-gated freshness (W-mpodww9h000b460a): bare TTL is not enough because
|
|
1807
|
-
// callers like tests and `safeWrite` write the central + per-project files
|
|
1808
|
-
// without going through `mutateWorkItems` (which busts the cache via
|
|
1809
|
-
// `invalidateWorkItemsCache`). One cheap `statSync` per file per cache hit
|
|
1810
|
-
// catches those writes within ~10 ms regardless of the TTL. Files that
|
|
1811
|
-
// don't exist record `null` so subsequent appearance still busts.
|
|
1812
1843
|
let _workItemsCache = null;
|
|
1813
1844
|
let _workItemsCacheAt = 0;
|
|
1814
|
-
let _workItemsCacheMtimes = null;
|
|
1815
1845
|
// Lean cache for getWorkItems(config, { enrich: false }) — core fields only,
|
|
1816
1846
|
// skipping the PR cross-reference + per-item _artifacts/_notes detail-modal
|
|
1817
1847
|
// enrichment (the synchronous KB-scan + agent-dir reads that the detail modal
|
|
@@ -1822,41 +1852,11 @@ let _workItemsCacheMtimes = null;
|
|
|
1822
1852
|
// objects each call, so the two caches never share mutable references.
|
|
1823
1853
|
let _workItemsLeanCache = null;
|
|
1824
1854
|
let _workItemsLeanCacheAt = 0;
|
|
1825
|
-
let _workItemsLeanCacheMtimes = null;
|
|
1826
1855
|
function invalidateWorkItemsCache() {
|
|
1827
1856
|
_workItemsCache = null;
|
|
1828
1857
|
_workItemsCacheAt = 0;
|
|
1829
|
-
_workItemsCacheMtimes = null;
|
|
1830
1858
|
_workItemsLeanCache = null;
|
|
1831
1859
|
_workItemsLeanCacheAt = 0;
|
|
1832
|
-
_workItemsLeanCacheMtimes = null;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
function _workItemsFilePaths(config) {
|
|
1836
|
-
const projects = getProjects(config);
|
|
1837
|
-
const out = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
1838
|
-
for (const p of projects) {
|
|
1839
|
-
try { out.push(projectWorkItemsPath(p)); } catch { /* missing path helper output */ }
|
|
1840
|
-
}
|
|
1841
|
-
return out;
|
|
1842
|
-
}
|
|
1843
|
-
function _snapshotWorkItemsMtimes(config) {
|
|
1844
|
-
const out = Object.create(null);
|
|
1845
|
-
for (const fp of _workItemsFilePaths(config)) {
|
|
1846
|
-
try { out[fp] = fs.statSync(fp).mtimeMs; }
|
|
1847
|
-
catch { out[fp] = null; /* ENOENT → null; flipping to present must bust */ }
|
|
1848
|
-
}
|
|
1849
|
-
return out;
|
|
1850
|
-
}
|
|
1851
|
-
function _workItemsMtimesDiffer(prev, curr) {
|
|
1852
|
-
if (!prev || !curr) return true;
|
|
1853
|
-
for (const fp of Object.keys(curr)) {
|
|
1854
|
-
if (prev[fp] !== curr[fp]) return true;
|
|
1855
|
-
}
|
|
1856
|
-
for (const fp of Object.keys(prev)) {
|
|
1857
|
-
if (!(fp in curr)) return true;
|
|
1858
|
-
}
|
|
1859
|
-
return false;
|
|
1860
1860
|
}
|
|
1861
1861
|
|
|
1862
1862
|
// getWorkItems(config, { enrich })
|
|
@@ -1874,13 +1874,9 @@ function getWorkItems(config, opts) {
|
|
|
1874
1874
|
const now = Date.now();
|
|
1875
1875
|
config = config || getConfig();
|
|
1876
1876
|
if (enrich) {
|
|
1877
|
-
if (_workItemsCache && (now - _workItemsCacheAt) < 1000)
|
|
1878
|
-
const currMtimes = _snapshotWorkItemsMtimes(config);
|
|
1879
|
-
if (!_workItemsMtimesDiffer(_workItemsCacheMtimes, currMtimes)) return _workItemsCache;
|
|
1880
|
-
}
|
|
1877
|
+
if (_workItemsCache && (now - _workItemsCacheAt) < 1000) return _workItemsCache;
|
|
1881
1878
|
} else if (_workItemsLeanCache && (now - _workItemsLeanCacheAt) < 1000) {
|
|
1882
|
-
|
|
1883
|
-
if (!_workItemsMtimesDiffer(_workItemsLeanCacheMtimes, currMtimes)) return _workItemsLeanCache;
|
|
1879
|
+
return _workItemsLeanCache;
|
|
1884
1880
|
}
|
|
1885
1881
|
let allItems = [];
|
|
1886
1882
|
|
|
@@ -1890,10 +1886,10 @@ function getWorkItems(config, opts) {
|
|
|
1890
1886
|
|
|
1891
1887
|
// Cross-reference with dispatch (fill in agent from active dispatch if missing on work item)
|
|
1892
1888
|
const dispatch = getDispatch();
|
|
1893
|
-
const activeByWiId = new Map((dispatch.active || []).map(d => [d
|
|
1889
|
+
const activeByWiId = new Map((dispatch.active || []).map(d => [_dispatchWorkItemIdentityKey(d), d.agent]));
|
|
1894
1890
|
for (const item of allItems) {
|
|
1895
1891
|
if (item.status === WI_STATUS.DISPATCHED && !item.dispatched_to && !item.agent) {
|
|
1896
|
-
const activeAgent = activeByWiId.get(item
|
|
1892
|
+
const activeAgent = activeByWiId.get(_workItemIdentityKey(item));
|
|
1897
1893
|
if (activeAgent) item.dispatched_to = activeAgent;
|
|
1898
1894
|
}
|
|
1899
1895
|
}
|
|
@@ -1902,7 +1898,7 @@ function getWorkItems(config, opts) {
|
|
|
1902
1898
|
const pendingByWiId = new Map();
|
|
1903
1899
|
for (const d of (dispatch.pending || [])) {
|
|
1904
1900
|
if (d.meta?.item?.id && d.skipReason) {
|
|
1905
|
-
pendingByWiId.set(d
|
|
1901
|
+
pendingByWiId.set(_dispatchWorkItemIdentityKey(d), d);
|
|
1906
1902
|
}
|
|
1907
1903
|
}
|
|
1908
1904
|
if (pendingByWiId.size > 0) {
|
|
@@ -1912,7 +1908,7 @@ function getWorkItems(config, opts) {
|
|
|
1912
1908
|
if (d.meta?.branch) branchToAgent.set(d.meta.branch, d.agentName || d.agent || '');
|
|
1913
1909
|
}
|
|
1914
1910
|
for (const item of allItems) {
|
|
1915
|
-
const pendingEntry = pendingByWiId.get(item
|
|
1911
|
+
const pendingEntry = pendingByWiId.get(_workItemIdentityKey(item));
|
|
1916
1912
|
if (!pendingEntry) continue;
|
|
1917
1913
|
item._skipReason = pendingEntry.skipReason;
|
|
1918
1914
|
if (pendingEntry.skipReason === 'branch_locked' && pendingEntry.meta?.branch) {
|
|
@@ -1953,7 +1949,7 @@ function getWorkItems(config, opts) {
|
|
|
1953
1949
|
// project so cross-repo branch collisions don't false-link.
|
|
1954
1950
|
// (W-mpn0b76200044eed)
|
|
1955
1951
|
const itemProject = item.project || item._source;
|
|
1956
|
-
const linkedPr = allPrs.find(p => (p.prdItems || []).includes(item.id))
|
|
1952
|
+
const linkedPr = allPrs.find(p => p._project === itemProject && (p.prdItems || []).includes(item.id))
|
|
1957
1953
|
|| (item.branch && allPrs.find(p => p.branch === item.branch && p._project === itemProject));
|
|
1958
1954
|
if (linkedPr) {
|
|
1959
1955
|
item._pr = linkedPr.id;
|
|
@@ -1966,12 +1962,12 @@ function getWorkItems(config, opts) {
|
|
|
1966
1962
|
// Build dispatch ID → work item ID lookup from completed dispatches
|
|
1967
1963
|
const dispatchByWiId = {};
|
|
1968
1964
|
for (const d of (dispatch.completed || [])) {
|
|
1969
|
-
const
|
|
1970
|
-
if (
|
|
1965
|
+
const key = _dispatchWorkItemIdentityKey(d);
|
|
1966
|
+
if (key) dispatchByWiId[key] = d.id; // last completed dispatch wins
|
|
1971
1967
|
}
|
|
1972
1968
|
for (const d of (dispatch.active || [])) {
|
|
1973
|
-
const
|
|
1974
|
-
if (
|
|
1969
|
+
const key = _dispatchWorkItemIdentityKey(d);
|
|
1970
|
+
if (key) dispatchByWiId[key] = d.id;
|
|
1975
1971
|
}
|
|
1976
1972
|
const _agentDirCache = {};
|
|
1977
1973
|
const _inboxFiles = safeReadDir(INBOX_DIR);
|
|
@@ -1993,13 +1989,17 @@ function getWorkItems(config, opts) {
|
|
|
1993
1989
|
const _archiveByAgent = {}; // agentId → archive filenames that include agentId
|
|
1994
1990
|
// P-34fa5d79 — pre-build the wiId → notes map so each item's _notes lookup
|
|
1995
1991
|
// is O(1) instead of re-scanning inbox+archive per item.
|
|
1996
|
-
const _notesByWi = _buildNotesByWiMap();
|
|
1992
|
+
const _notesByWi = _buildNotesByWiMap(dispatch);
|
|
1993
|
+
const _workItemIdCounts = new Map();
|
|
1994
|
+
for (const item of allItems) {
|
|
1995
|
+
_workItemIdCounts.set(item.id, (_workItemIdCounts.get(item.id) || 0) + 1);
|
|
1996
|
+
}
|
|
1997
1997
|
for (const item of allItems) {
|
|
1998
1998
|
const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
|
|
1999
1999
|
const agentId = item.dispatched_to || item.agent;
|
|
2000
2000
|
if (agentId) {
|
|
2001
2001
|
// Output log — match by dispatch ID (output-{dispatchId}.log)
|
|
2002
|
-
const dispatchId = dispatchByWiId[item
|
|
2002
|
+
const dispatchId = dispatchByWiId[_workItemIdentityKey(item)];
|
|
2003
2003
|
if (dispatchId) {
|
|
2004
2004
|
if (!_agentDirCache[agentId]) {
|
|
2005
2005
|
_agentDirCache[agentId] = safeReadDir(path.join(MINIONS_DIR, 'agents', agentId)).filter(f => f.startsWith('output-') && f.endsWith('.log'));
|
|
@@ -2009,14 +2009,19 @@ function getWorkItems(config, opts) {
|
|
|
2009
2009
|
}
|
|
2010
2010
|
// Notes: inbox → KB (via source field) → archive (fallback if KB was swept)
|
|
2011
2011
|
const itemId = item.id || '___';
|
|
2012
|
-
const
|
|
2012
|
+
const hasUniqueId = _workItemIdCounts.get(itemId) === 1;
|
|
2013
|
+
const matchInbox = hasUniqueId
|
|
2014
|
+
? _inboxFiles.filter(f => f.includes(agentId) && f.includes(itemId))
|
|
2015
|
+
: [];
|
|
2013
2016
|
// Lazily bucket KB by agentId (computed once per distinct agent), then
|
|
2014
2017
|
// filter the small bucket by itemId — same result as filtering all of
|
|
2015
2018
|
// _kbEntries by (agentId AND itemId), without the per-item full scan.
|
|
2016
2019
|
if (_kbByAgent[agentId] === undefined) {
|
|
2017
2020
|
_kbByAgent[agentId] = _kbEntries.filter(kb => kb.source && kb.source.includes(agentId));
|
|
2018
2021
|
}
|
|
2019
|
-
const matchKb =
|
|
2022
|
+
const matchKb = hasUniqueId
|
|
2023
|
+
? _kbByAgent[agentId].filter(kb => kb.source.includes(itemId))
|
|
2024
|
+
: [];
|
|
2020
2025
|
const allNotes = [
|
|
2021
2026
|
...matchInbox,
|
|
2022
2027
|
...matchKb.map(kb => 'kb:' + kb.cat + '/' + kb.file),
|
|
@@ -2028,7 +2033,9 @@ function getWorkItems(config, opts) {
|
|
|
2028
2033
|
if (_archiveByAgent[agentId] === undefined) {
|
|
2029
2034
|
_archiveByAgent[agentId] = _archiveFiles.filter(f => f.includes(agentId));
|
|
2030
2035
|
}
|
|
2031
|
-
const matchArchive =
|
|
2036
|
+
const matchArchive = hasUniqueId
|
|
2037
|
+
? _archiveByAgent[agentId].filter(f => f.includes(itemId))
|
|
2038
|
+
: [];
|
|
2032
2039
|
for (const f of matchArchive) allNotes.push('archive:' + f);
|
|
2033
2040
|
}
|
|
2034
2041
|
if (allNotes.length > 0) arts.notes = _mergeArtifactNotes(arts.notes, allNotes);
|
|
@@ -2044,7 +2051,9 @@ function getWorkItems(config, opts) {
|
|
|
2044
2051
|
// prefixed with `archive:` so the dashboard chip renderer can pick the
|
|
2045
2052
|
// right open path. Field is left undefined (not []) when empty so the
|
|
2046
2053
|
// /api/work-items JSON stays compact for the common no-notes case.
|
|
2047
|
-
const
|
|
2054
|
+
const identityKey = _workItemIdentityKey(item);
|
|
2055
|
+
const mentions = _notesByWi[identityKey]
|
|
2056
|
+
|| (_workItemIdCounts.get(item.id) === 1 ? _notesByWi[item.id] : null);
|
|
2048
2057
|
if (mentions && mentions.length > 0) item._notes = mentions;
|
|
2049
2058
|
}
|
|
2050
2059
|
} // end if (enrich)
|
|
@@ -2064,7 +2073,7 @@ function getWorkItems(config, opts) {
|
|
|
2064
2073
|
return (b.created || '').localeCompare(a.created || '');
|
|
2065
2074
|
});
|
|
2066
2075
|
|
|
2067
|
-
|
|
2076
|
+
|
|
2068
2077
|
// Use Date.now() here (not the `now` captured at function entry) so the
|
|
2069
2078
|
// 1 s TTL is measured from when the cache was BUILT, not from when the
|
|
2070
2079
|
// call started. On a slow or loaded machine the first cold call can take
|
|
@@ -2076,11 +2085,9 @@ function getWorkItems(config, opts) {
|
|
|
2076
2085
|
if (enrich) {
|
|
2077
2086
|
_workItemsCache = allItems;
|
|
2078
2087
|
_workItemsCacheAt = cacheSetAt;
|
|
2079
|
-
_workItemsCacheMtimes = cacheMtimes;
|
|
2080
2088
|
} else {
|
|
2081
2089
|
_workItemsLeanCache = allItems;
|
|
2082
2090
|
_workItemsLeanCacheAt = cacheSetAt;
|
|
2083
|
-
_workItemsLeanCacheMtimes = cacheMtimes;
|
|
2084
2091
|
}
|
|
2085
2092
|
return allItems;
|
|
2086
2093
|
}
|
|
@@ -2234,31 +2241,17 @@ function getPrdInfo(config) {
|
|
|
2234
2241
|
// so they don't masquerade as a 2nd PRD item in the aggregate guard (W-mpem52qn).
|
|
2235
2242
|
const wiById = {};
|
|
2236
2243
|
const allWiById = {};
|
|
2237
|
-
for (const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
if (!wi?.id) { console.warn(`[queries] Skipping work item without id in ${project.name}:`, JSON.stringify(wi).slice(0, 120)); continue; }
|
|
2242
|
-
if (!allWiById[wi.id]) allWiById[wi.id] = wi;
|
|
2243
|
-
if (wi.sourcePlan) wiById[wi.id] = wi;
|
|
2244
|
-
}
|
|
2245
|
-
} catch { /* optional */ }
|
|
2246
|
-
}
|
|
2247
|
-
// Also check central work-items.json
|
|
2248
|
-
try {
|
|
2249
|
-
const centralWi = readJsonNoRestore(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
2250
|
-
for (const wi of centralWi) {
|
|
2251
|
-
if (!wi?.id) { console.warn('[queries] Skipping central work item without id:', JSON.stringify(wi).slice(0, 120)); continue; }
|
|
2252
|
-
if (!allWiById[wi.id]) allWiById[wi.id] = wi;
|
|
2253
|
-
if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi;
|
|
2244
|
+
for (const wi of require('./work-items-store').readAllWorkItems()) {
|
|
2245
|
+
if (!wi?.id) {
|
|
2246
|
+
console.warn(`[queries] Skipping work item without id in ${wi?._source || 'unknown'}:`, JSON.stringify(wi).slice(0, 120));
|
|
2247
|
+
continue;
|
|
2254
2248
|
}
|
|
2255
|
-
|
|
2249
|
+
if (!allWiById[wi.id]) allWiById[wi.id] = wi;
|
|
2250
|
+
if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi;
|
|
2251
|
+
}
|
|
2256
2252
|
|
|
2257
|
-
//
|
|
2258
|
-
//
|
|
2259
|
-
// than the feature-id string. The FK itself lives only in SQL (it's not in the
|
|
2260
|
-
// JSON mirror), so read work_items.prd_item_id once and join it to the WI
|
|
2261
|
-
// objects we already loaded, keyed by wi.id (first-wins, matching allWiById).
|
|
2253
|
+
// Index work items by their stable SQL FK so the per-item join can resolve
|
|
2254
|
+
// PRD item → WI by prd_items.id rather than the feature-id string.
|
|
2262
2255
|
// Best-effort: null map → the join falls back to the legacy string match.
|
|
2263
2256
|
let wiByPrdItemId = null;
|
|
2264
2257
|
if (useFkJoin && prdItemIdByKey) {
|
|
@@ -2325,7 +2318,7 @@ function getPrdInfo(config) {
|
|
|
2325
2318
|
for (const itemId of (itemIds || [])) {
|
|
2326
2319
|
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
2327
2320
|
// W-mrcs8yqk001o6893 — when the pr_links mapping outlives the PR record
|
|
2328
|
-
// (merged-PR sweep removes it from
|
|
2321
|
+
// (merged-PR sweep removes it from tracked PR state but pr_links is
|
|
2329
2322
|
// never pruned), resolve the item's project so we can self-heal the
|
|
2330
2323
|
// status the same way the wi._pr fallback loop below does.
|
|
2331
2324
|
let itemProject = null;
|
|
@@ -2419,7 +2412,7 @@ function getPrdInfo(config) {
|
|
|
2419
2412
|
const missing = (byStatus['missing'] || []).length;
|
|
2420
2413
|
const donePercent = total > 0 ? Math.round((complete / total) * 100) : 0;
|
|
2421
2414
|
|
|
2422
|
-
// Plan timings —
|
|
2415
|
+
// Plan timings — wiById already includes the central scope.
|
|
2423
2416
|
const planTimings = {};
|
|
2424
2417
|
for (const wi of Object.values(wiById)) {
|
|
2425
2418
|
if (!wi.sourcePlan) continue;
|
|
@@ -3075,71 +3068,8 @@ function _getProjectGitStatusCacheForTest() {
|
|
|
3075
3068
|
return _projectGitStatusCache;
|
|
3076
3069
|
}
|
|
3077
3070
|
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
* (W-mpftp7na000td0f4). Single source of truth for the dashboard's
|
|
3081
|
-
* cache-invalidation tracker — adding a new fast-state-surfaced JSON file
|
|
3082
|
-
* is a **one-line change here**, next to the related fast-state getters.
|
|
3083
|
-
*
|
|
3084
|
-
* Contract for additions:
|
|
3085
|
-
* 1. The file must be read (directly or transitively) by something in
|
|
3086
|
-
* `dashboard._buildStatusFastState` (e.g. `getPullRequests`,
|
|
3087
|
-
* `getDispatchQueue`, `watchesMod.getWatches`). Files surfaced only
|
|
3088
|
-
* in `_buildStatusSlowState` (60 s TTL) should NOT be added — fast-
|
|
3089
|
-
* state mtime invalidation has no effect on slow state, and they
|
|
3090
|
-
* will silently no-op.
|
|
3091
|
-
* 2. The file must be mutated through a `mutate*` helper (so writes
|
|
3092
|
-
* actually advance mtime via `safeWrite`'s rename). Append-only logs
|
|
3093
|
-
* can also be added, but expect rebuilds on every line write.
|
|
3094
|
-
* 3. Per-project files must use `shared.projectPrPath` / `shared.
|
|
3095
|
-
* projectWorkItemsPath` so newly-added projects are picked up
|
|
3096
|
-
* automatically.
|
|
3097
|
-
*
|
|
3098
|
-
* Files intentionally NOT tracked:
|
|
3099
|
-
* - `engine/log.json` — append-only log written many times per tick
|
|
3100
|
-
* (PR polls, dispatch transitions, heartbeat). Its mtime advances
|
|
3101
|
-
* every few seconds in steady state, which by itself was enough to
|
|
3102
|
-
* bust the ETag cache on every poll and reduce the /api/status 304
|
|
3103
|
-
* short-circuit to 0% hit rate (W-mpg8aapw001d7e0c). Log tail is
|
|
3104
|
-
* surfaced by `getEngineLog()` and accepts the 10 s `FAST_STATE_TTL`
|
|
3105
|
-
* backstop — log entries are not UX-blocking.
|
|
3106
|
-
* - `engine/control.json` — heartbeat writes advance mtime every tick
|
|
3107
|
-
* (~60 s) plus on any control-side mutation, dominating legitimate
|
|
3108
|
-
* state changes. Engine state (running/paused/stopped) is surfaced
|
|
3109
|
-
* by `getEngineState()`; the dashboard-side mutating handlers
|
|
3110
|
-
* (`handleEngineRestart` etc.) call `invalidateStatusCache()`
|
|
3111
|
-
* directly, and the 10 s TTL covers CLI-driven control-file edits.
|
|
3112
|
-
* - `engine/state.json` — surfaced via `getEngineState()` but changes
|
|
3113
|
-
* only on engine startup / reconcile. Negligible benefit.
|
|
3114
|
-
* - `engine/metrics.json` — surfaced via `getMetrics()` but written by
|
|
3115
|
-
* `trackEngineUsage()` / `trackAgentMetric()` / the engine tick itself
|
|
3116
|
-
* (~10 s batched flush plus every per-agent metric event). Like
|
|
3117
|
-
* `log.json` and `control.json`, its mtime advances at noise-floor
|
|
3118
|
-
* cadence — every engine tick (~60 s) plus several per-agent writes
|
|
3119
|
-
* in between, so with multiple agents active it bumps every few
|
|
3120
|
-
* seconds and busts the ETag cache on /api/status before steady-state
|
|
3121
|
-
* 304s can engage (W-mphejzct00065d8c). The engine-page metrics tile
|
|
3122
|
-
* is not the hot path and the 10 s `FAST_STATE_TTL` is plenty for
|
|
3123
|
-
* metric freshness; mutating handlers that need immediate metrics
|
|
3124
|
-
* visibility should call `invalidateStatusCache()` directly.
|
|
3125
|
-
* - `engine/cooldowns.json`, `engine/pr-links.json`, `engine/pending-
|
|
3126
|
-
* rebases.json`, `agents/<id>/managed-spawn.json` — not in the
|
|
3127
|
-
* `/api/status` payload.
|
|
3128
|
-
* - `pinned.md`, `schedules`, `pipeline-runs.json`, `schedule-runs.json`,
|
|
3129
|
-
* PRD JSON — slow-state only (see `getStatusSlowStateMtimePaths`).
|
|
3130
|
-
*
|
|
3131
|
-
* Files tracked PER-FILE rather than via dir mtime:
|
|
3132
|
-
* - `meetings/<id>.json` — round transitions edit each file in-place via
|
|
3133
|
-
* `engine/meeting.js#mutateMeeting`. The parent dir's mtime does not
|
|
3134
|
-
* advance on Windows NTFS for in-place edits inside an existing file,
|
|
3135
|
-
* so tracking each meeting JSON individually catches round/status flips
|
|
3136
|
-
* that dir mtime would miss. Bounded by active meeting count.
|
|
3137
|
-
*
|
|
3138
|
-
* Performance: `_getMtimes()` in dashboard.js does `fs.statSync` per path
|
|
3139
|
-
* per `getStatus()` call. Roughly N=4 engine paths + 2 per project today,
|
|
3140
|
-
* so a 5-project fleet runs 14 statSync calls per cache miss — bounded
|
|
3141
|
-
* and unmeasurable in benchmarks.
|
|
3142
|
-
*/
|
|
3071
|
+
// Runtime state invalidates through SQL events; these caches only track
|
|
3072
|
+
// intentionally file-backed dashboard inputs.
|
|
3143
3073
|
// Cache the slow-state path list: it only changes when projects are added/
|
|
3144
3074
|
// removed or runtimes are registered/deregistered (essentially process-
|
|
3145
3075
|
// lifetime stable in practice). Without it we walked runtime-registry +
|
|
@@ -3157,22 +3087,7 @@ function _mtimePathsCacheKey(config) {
|
|
|
3157
3087
|
return projects.join(';');
|
|
3158
3088
|
}
|
|
3159
3089
|
function getStatusFastStateMtimePaths(config) {
|
|
3160
|
-
//
|
|
3161
|
-
// skills/mcp/projects/version. dispatch.json, work-items.json,
|
|
3162
|
-
// pull-requests.json, watches.json, notes.md, INBOX_DIR, qa-runs.json,
|
|
3163
|
-
// and the meetings dir all moved to dedicated /api/<x> endpoints which
|
|
3164
|
-
// have their OWN input-mtime ETags. Continuing to track those files in
|
|
3165
|
-
// the fast-state registry would bust the /api/status outer cache on
|
|
3166
|
-
// every engine write to them — pure wasted rebuild for slices that no
|
|
3167
|
-
// longer appear in the payload.
|
|
3168
|
-
//
|
|
3169
|
-
// Fast-state inputs are now exclusively in-memory (engine heartbeat,
|
|
3170
|
-
// ADO/GH throttle counters) so no file-mtime tracking is required.
|
|
3171
|
-
// Returning [] makes the fast tracker a no-op; the slow tracker still
|
|
3172
|
-
// catches the few file-derived slices that remain (skills, mcp,
|
|
3173
|
-
// projects/git, autoMode/config). The cache version is bumped by
|
|
3174
|
-
// `emitStateEvent` (db-events) when the slow registry detects a
|
|
3175
|
-
// change OR when explicit invalidateStatusCache() callers fire.
|
|
3090
|
+
// Fast state is in memory or SQL, so file-mtime tracking is unnecessary.
|
|
3176
3091
|
config = config || getConfig();
|
|
3177
3092
|
const cacheKey = _mtimePathsCacheKey(config);
|
|
3178
3093
|
if (_fastMtimePathsCache && _fastMtimePathsCacheKey === cacheKey) return _fastMtimePathsCache;
|
|
@@ -3185,22 +3100,8 @@ function getStatusFastStateMtimePaths(config) {
|
|
|
3185
3100
|
/**
|
|
3186
3101
|
* Slow-state mtime tracker — symmetric with the fast-state registry above.
|
|
3187
3102
|
*
|
|
3188
|
-
* Slow-state
|
|
3189
|
-
*
|
|
3190
|
-
* `version`, etc.) live behind a 60 s TTL in `dashboard.js`. Without mtime
|
|
3191
|
-
* tracking, engine-driven writes to `prd/*.json`, `engine/schedule-runs.json`,
|
|
3192
|
-
* `engine/pipeline-runs.json`, or `prd/guides/*.md` waited up to the full
|
|
3193
|
-
* 60 s before surfacing in the dashboard — which produced the visible
|
|
3194
|
-
* "plan status doesn't update, pipelines never advance" symptom.
|
|
3195
|
-
*
|
|
3196
|
-
* The same conventions as fast-state apply:
|
|
3197
|
-
* - Entries must back something `_buildStatusSlowState()` reads; otherwise
|
|
3198
|
-
* the rebuild silently no-ops on detected change.
|
|
3199
|
-
* - Files mutated through a `mutate*` helper are reliable (`safeWrite`
|
|
3200
|
-
* rename advances mtime). Append-only logs reset the cache often but
|
|
3201
|
-
* are not relevant here.
|
|
3202
|
-
* - Per-project paths must use `shared.project*` so newly-added projects
|
|
3203
|
-
* are picked up without registry edits.
|
|
3103
|
+
* Slow-state mtime tracking covers only intentionally file-backed inputs such
|
|
3104
|
+
* as config, skills, MCP settings, and project git metadata.
|
|
3204
3105
|
*
|
|
3205
3106
|
* Skill and MCP source paths (W-mphfdgwv000bf549):
|
|
3206
3107
|
* - Skill discovery roots (`~/.claude/skills`, `~/.copilot/skills`,
|
|
@@ -3332,7 +3233,7 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
3332
3233
|
module.exports = {
|
|
3333
3234
|
// Paths (for modules that need direct access)
|
|
3334
3235
|
MINIONS_DIR, AGENTS_DIR, ENGINE_DIR, INBOX_DIR, PLANS_DIR, PRD_DIR, SKILLS_DIR, KNOWLEDGE_DIR, ARCHIVE_DIR,
|
|
3335
|
-
CONFIG_PATH, CONTROL_PATH,
|
|
3236
|
+
CONFIG_PATH, CONTROL_PATH, NOTES_PATH,
|
|
3336
3237
|
|
|
3337
3238
|
// Helpers
|
|
3338
3239
|
timeSince,
|