@yemi33/minions 0.1.2378 → 0.1.2380
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 +2 -3
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard.js +393 -721
- package/docs/README.md +1 -0
- 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-inbox-entries-schema.md +31 -39
- 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/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/acp-transport.js +49 -8
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +124 -15
- 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 +34 -41
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +221 -283
- 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 +1 -1
- package/engine/pooled-agent-process.js +46 -26
- 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 +3 -5
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +132 -637
- 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 +167 -349
- 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 +2 -2
package/engine/cleanup.js
CHANGED
|
@@ -7,16 +7,16 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
|
+
const smallStateStore = require('./small-state-store');
|
|
10
11
|
|
|
11
12
|
const { exec, execAsync, execSilent, log, ts, ENGINE_DEFAULTS } = shared;
|
|
12
|
-
const { safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, safeReadDir,
|
|
13
|
+
const { safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, safeReadDir, readWorkItems, readPullRequests, mutateCooldowns, mutateMetrics, mutateWorkItems, getProjects,
|
|
13
14
|
sanitizeBranch, KB_CATEGORIES } = shared;
|
|
14
15
|
const { getDispatch, getAgentStatus } = queries;
|
|
15
16
|
|
|
16
17
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
17
18
|
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
18
19
|
const ENGINE_DIR = queries.ENGINE_DIR;
|
|
19
|
-
const PRD_DIR = queries.PRD_DIR;
|
|
20
20
|
const PLANS_DIR = queries.PLANS_DIR;
|
|
21
21
|
|
|
22
22
|
// Lazy require to break circular dependency with engine.js
|
|
@@ -98,7 +98,7 @@ function localBranchWorktreeInUse(root, branch) {
|
|
|
98
98
|
function collectPhantomBranchesForProject(project) {
|
|
99
99
|
const branches = new Set();
|
|
100
100
|
try {
|
|
101
|
-
const items =
|
|
101
|
+
const items = readWorkItems(project);
|
|
102
102
|
if (!Array.isArray(items)) return branches;
|
|
103
103
|
for (const w of items) {
|
|
104
104
|
if (w && w._phantomCompletion === true && w._phantomBranch) {
|
|
@@ -187,7 +187,7 @@ function cleanupMergedPrLocalBranch(root, project, pr) {
|
|
|
187
187
|
* env var is unset, or that fail before cleanup, leave `.json` + `.json.backup`
|
|
188
188
|
* sidecars behind). The engine's tick loop then re-discovers these fixtures
|
|
189
189
|
* every tick, fails playbook validation ("missing required template variables:
|
|
190
|
-
* agenda"), and spams log
|
|
190
|
+
* agenda"), and spams the engine log with the same error.
|
|
191
191
|
*
|
|
192
192
|
* Filter is conservative: only files whose ID begins with `TEST-` AND whose
|
|
193
193
|
* JSON either lacks an `agenda` or has an empty/whitespace-only one. Both the
|
|
@@ -206,41 +206,14 @@ function cleanupMergedPrLocalBranch(root, project, pr) {
|
|
|
206
206
|
// `.json` — and only when SQLite is healthy so backups remain the legacy
|
|
207
207
|
// fallback for users on Node < 22.5.
|
|
208
208
|
//
|
|
209
|
-
|
|
210
|
-
// table to that table; safe-by-age files have no SQL backing but their
|
|
211
|
-
// .backup sidecars are equally useless once SQLite is the authority.
|
|
212
|
-
|
|
213
|
-
const ENGINE_SQL_BACKED_BACKUPS = [
|
|
214
|
-
{ file: 'dispatch.json', table: 'dispatches' },
|
|
215
|
-
{ file: 'metrics.json', table: 'metrics' },
|
|
216
|
-
{ file: 'watches.json', table: 'watches' },
|
|
217
|
-
{ file: 'schedule-runs.json', table: 'schedule_runs' },
|
|
218
|
-
{ file: 'pipeline-runs.json', table: 'pipeline_runs' },
|
|
219
|
-
{ file: 'managed-processes.json', table: 'managed_processes' },
|
|
220
|
-
{ file: 'log.json', table: 'logs' },
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
const ENGINE_SAFE_BY_AGE_BACKUPS = [
|
|
224
|
-
'cc-sessions.json',
|
|
209
|
+
const ENGINE_FILE_BACKED_BACKUPS = [
|
|
225
210
|
'control.json',
|
|
226
|
-
'cooldowns.json',
|
|
227
211
|
'dashboard-browser.json',
|
|
228
|
-
'doc-sessions.json',
|
|
229
212
|
'kb-pins.json',
|
|
230
|
-
'pending-rebases.json',
|
|
231
|
-
'pr-links.json',
|
|
232
|
-
'qa-sessions.json',
|
|
233
213
|
];
|
|
234
214
|
|
|
235
215
|
const MEETINGS_BACKUP_FRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
236
216
|
|
|
237
|
-
function _tableHasRow(db, table) {
|
|
238
|
-
try {
|
|
239
|
-
const row = db.prepare(`SELECT 1 FROM ${table} LIMIT 1`).get();
|
|
240
|
-
return !!row;
|
|
241
|
-
} catch { return false; }
|
|
242
|
-
}
|
|
243
|
-
|
|
244
217
|
function _tryPruneBackup(liveFile, pruned) {
|
|
245
218
|
const backupPath = liveFile + '.backup';
|
|
246
219
|
if (!fs.existsSync(backupPath)) return;
|
|
@@ -253,32 +226,7 @@ function _tryPruneBackup(liveFile, pruned) {
|
|
|
253
226
|
|
|
254
227
|
function pruneStaleBackupSidecars(config) {
|
|
255
228
|
const pruned = { count: 0 };
|
|
256
|
-
|
|
257
|
-
try { db = require('./db').getDb(); }
|
|
258
|
-
catch { return pruned; } // SQLite unavailable — keep backups as the legacy fallback
|
|
259
|
-
|
|
260
|
-
// (1a) SQL-backed engine state files — require the table to be populated.
|
|
261
|
-
for (const { file, table } of ENGINE_SQL_BACKED_BACKUPS) {
|
|
262
|
-
if (!_tableHasRow(db, table)) continue;
|
|
263
|
-
_tryPruneBackup(path.join(ENGINE_DIR, file), pruned);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// (1b) Per-project work-items.json + pull-requests.json — both share
|
|
267
|
-
// global SQL tables, so populate-check those tables once.
|
|
268
|
-
const wiHasRow = _tableHasRow(db, 'work_items');
|
|
269
|
-
const prHasRow = _tableHasRow(db, 'pull_requests');
|
|
270
|
-
if (wiHasRow || prHasRow) {
|
|
271
|
-
for (const project of getProjects(config)) {
|
|
272
|
-
try {
|
|
273
|
-
if (wiHasRow) _tryPruneBackup(projectWorkItemsPath(project), pruned);
|
|
274
|
-
if (prHasRow) _tryPruneBackup(projectPrPath(project), pruned);
|
|
275
|
-
} catch { /* malformed project entry — skip */ }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// (1c) Safe-by-age set — no SQL table backing, but the .backup sidecars
|
|
280
|
-
// are equally useless once SQLite is healthy.
|
|
281
|
-
for (const file of ENGINE_SAFE_BY_AGE_BACKUPS) {
|
|
229
|
+
for (const file of ENGINE_FILE_BACKED_BACKUPS) {
|
|
282
230
|
_tryPruneBackup(path.join(ENGINE_DIR, file), pruned);
|
|
283
231
|
}
|
|
284
232
|
|
|
@@ -528,7 +476,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
528
476
|
if (e?.tmpDir) activeDispatchTmpDirs.add(path.resolve(e.tmpDir));
|
|
529
477
|
}
|
|
530
478
|
}
|
|
531
|
-
} catch { /* dispatch
|
|
479
|
+
} catch { /* dispatch state may be empty */ }
|
|
532
480
|
for (const dir of scanDirs) {
|
|
533
481
|
// Each directory gets its own try-catch so one failure doesn't abort other directories (Bug #27)
|
|
534
482
|
let dirEntries;
|
|
@@ -600,11 +548,11 @@ async function runCleanup(config, verbose = false) {
|
|
|
600
548
|
}
|
|
601
549
|
|
|
602
550
|
// 2c. Reap orphan agents/temp-* dirs whose dispatch is no longer referenced
|
|
603
|
-
// anywhere in dispatch.
|
|
551
|
+
// anywhere in dispatch state. Temp-agent dirs are created by the engine for
|
|
604
552
|
// ephemeral temp-<uid> agents; once the dispatch ages out of dispatch
|
|
605
553
|
// history they're never touched again, accumulating MB of live-output.log
|
|
606
554
|
// tails over weeks. 1h mtime gate prevents reaping a still-spawning temp
|
|
607
|
-
// agent that races dispatch
|
|
555
|
+
// agent that races dispatch-state visibility.
|
|
608
556
|
cleaned.orphanTempAgentDirs = 0;
|
|
609
557
|
try {
|
|
610
558
|
const dispatch = getDispatch();
|
|
@@ -766,6 +714,12 @@ async function runCleanup(config, verbose = false) {
|
|
|
766
714
|
// 3. Clean git worktrees for merged/abandoned PRs
|
|
767
715
|
const _attemptedWorktreePaths = new Set(); // dedup across projects sharing a worktreeRoot
|
|
768
716
|
const _reapedScratchRoots = new Set(); // reap each worktree root's .agent-temp once per cleanup
|
|
717
|
+
let activeSharedBranchPlans = [];
|
|
718
|
+
try {
|
|
719
|
+
activeSharedBranchPlans = require('./prd-store').listPrdRows()
|
|
720
|
+
.map(row => row.plan)
|
|
721
|
+
.filter(plan => plan?.branch_strategy === 'shared-branch' && plan.feature_branch && plan.status !== 'completed');
|
|
722
|
+
} catch { /* optional */ }
|
|
769
723
|
cleaned.agentScratch = 0;
|
|
770
724
|
for (const project of projects) {
|
|
771
725
|
const root = project.localPath ? path.resolve(project.localPath) : null;
|
|
@@ -788,12 +742,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
788
742
|
try { cleaned.agentScratch += reapAgentScratch(worktreeRoot); } catch (_e) { /* best-effort */ }
|
|
789
743
|
}
|
|
790
744
|
|
|
791
|
-
|
|
792
|
-
// Array.isArray guard: safeJson(p) || [] only substitutes [] for falsy values;
|
|
793
|
-
// a truthy non-array (e.g. {}, 42, true) bypasses the fallback and causes a
|
|
794
|
-
// TypeError in the for-of loop below. Array.isArray covers that case.
|
|
795
|
-
const _prsRaw = safeJson(projectPrPath(project));
|
|
796
|
-
const prs = Array.isArray(_prsRaw) ? _prsRaw : [];
|
|
745
|
+
const prs = readPullRequests(project);
|
|
797
746
|
const mergedBranches = new Set();
|
|
798
747
|
for (const pr of prs) {
|
|
799
748
|
if (pr.status === shared.PR_STATUS.MERGED || pr.status === shared.PR_STATUS.ABANDONED) {
|
|
@@ -856,7 +805,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
856
805
|
// `cwd` inside the dispatch's worktree; the agent is gone so no
|
|
857
806
|
// active dispatch references the branch and the >2h age sweep would
|
|
858
807
|
// reap the worktree from under the live services. Build a list of
|
|
859
|
-
// normalized
|
|
808
|
+
// normalized cwd values from managed-process state and protect any
|
|
860
809
|
// worktree dir that contains one. Cwd is optional per the schema, so
|
|
861
810
|
// entries without cwd contribute nothing.
|
|
862
811
|
const _managedSpawnCwds = [];
|
|
@@ -970,27 +919,18 @@ async function runCleanup(config, verbose = false) {
|
|
|
970
919
|
} catch { /* optional */ }
|
|
971
920
|
}
|
|
972
921
|
|
|
973
|
-
// Skip worktrees for active shared-branch plans
|
|
922
|
+
// Skip worktrees for active shared-branch plans.
|
|
974
923
|
if (shouldClean || !isProtected) {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (
|
|
981
|
-
if (worktreeMatchesBranch(dirLower, plan.feature_branch, actualBranch)) {
|
|
982
|
-
isProtected = true;
|
|
983
|
-
if (shouldClean) {
|
|
984
|
-
shouldClean = false;
|
|
985
|
-
if (verbose) console.log(` Skipping worktree ${dir}: active shared-branch plan`);
|
|
986
|
-
}
|
|
987
|
-
break;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
924
|
+
for (const plan of activeSharedBranchPlans) {
|
|
925
|
+
if (worktreeMatchesBranch(dirLower, plan.feature_branch, actualBranch)) {
|
|
926
|
+
isProtected = true;
|
|
927
|
+
if (shouldClean) {
|
|
928
|
+
shouldClean = false;
|
|
929
|
+
if (verbose) console.log(` Skipping worktree ${dir}: active shared-branch plan`);
|
|
990
930
|
}
|
|
991
|
-
|
|
931
|
+
break;
|
|
992
932
|
}
|
|
993
|
-
}
|
|
933
|
+
}
|
|
994
934
|
}
|
|
995
935
|
|
|
996
936
|
wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected, actualBranch, matchedMergedBranch });
|
|
@@ -1011,10 +951,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
1011
951
|
// Remove all marked worktrees
|
|
1012
952
|
// Re-read PR status immediately before deletion — a PR can be reopened between
|
|
1013
953
|
// the initial status check and the actual deletion (Bug #15: TOCTOU race)
|
|
1014
|
-
|
|
1015
|
-
// bypasses || [] and would throw inside the try-catch, silently skipping deletions.
|
|
1016
|
-
const _freshPrsRaw = safeJson(projectPrPath(project));
|
|
1017
|
-
const freshPrs = Array.isArray(_freshPrsRaw) ? _freshPrsRaw : [];
|
|
954
|
+
const freshPrs = readPullRequests(project);
|
|
1018
955
|
const freshMergedBranches = new Set();
|
|
1019
956
|
const freshMergedPrByBranch = new Map();
|
|
1020
957
|
for (const pr of freshPrs) {
|
|
@@ -1280,9 +1217,8 @@ async function runCleanup(config, verbose = false) {
|
|
|
1280
1217
|
// race conditions with concurrent engine/dashboard/lifecycle writers.
|
|
1281
1218
|
for (const project of projects) {
|
|
1282
1219
|
try {
|
|
1283
|
-
const wiPath = projectWorkItemsPath(project);
|
|
1284
1220
|
let reconciled = 0;
|
|
1285
|
-
mutateWorkItems(
|
|
1221
|
+
mutateWorkItems(project, items => {
|
|
1286
1222
|
for (const item of items) {
|
|
1287
1223
|
if (item.status === shared.WI_STATUS.FAILED && item._pr) {
|
|
1288
1224
|
item.status = shared.WI_STATUS.DONE;
|
|
@@ -1323,10 +1259,10 @@ async function runCleanup(config, verbose = false) {
|
|
|
1323
1259
|
}
|
|
1324
1260
|
return false;
|
|
1325
1261
|
}
|
|
1326
|
-
function _migrateLegacyItemsAt(
|
|
1262
|
+
function _migrateLegacyItemsAt(scope, label) {
|
|
1327
1263
|
try {
|
|
1328
1264
|
let migrated = 0;
|
|
1329
|
-
mutateWorkItems(
|
|
1265
|
+
mutateWorkItems(scope, items => {
|
|
1330
1266
|
for (const item of items) if (_migrateLegacyItem(item)) migrated++;
|
|
1331
1267
|
});
|
|
1332
1268
|
if (migrated > 0) {
|
|
@@ -1335,12 +1271,9 @@ async function runCleanup(config, verbose = false) {
|
|
|
1335
1271
|
// gate ("legacy-done-aliases" removal after 30 consecutive days zero
|
|
1336
1272
|
// migrations) measurable by bumping a rolling-daily counter under
|
|
1337
1273
|
// `_engine.legacyStatusMigrations[YYYY-MM-DD]`.
|
|
1338
|
-
// Inline mutateJsonFileLocked by design — promote to a shared
|
|
1339
|
-
// `mutateMetrics(key, date)` helper if more N-day-silence gates land.
|
|
1340
1274
|
try {
|
|
1341
|
-
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
1342
1275
|
const dateKey = new Date().toISOString().slice(0, 10);
|
|
1343
|
-
|
|
1276
|
+
mutateMetrics((metrics) => {
|
|
1344
1277
|
metrics = metrics || {};
|
|
1345
1278
|
if (!metrics._engine) metrics._engine = {};
|
|
1346
1279
|
if (!metrics._engine.legacyStatusMigrations) metrics._engine.legacyStatusMigrations = {};
|
|
@@ -1354,15 +1287,14 @@ async function runCleanup(config, verbose = false) {
|
|
|
1354
1287
|
}
|
|
1355
1288
|
} catch (e) { log('warn', `migrate legacy statuses (${label}): ${e.message}`); }
|
|
1356
1289
|
}
|
|
1357
|
-
for (const project of projects) _migrateLegacyItemsAt(
|
|
1358
|
-
_migrateLegacyItemsAt(
|
|
1290
|
+
for (const project of projects) _migrateLegacyItemsAt(project, `${project.name} work items`);
|
|
1291
|
+
_migrateLegacyItemsAt('central', 'central work items');
|
|
1359
1292
|
|
|
1360
1293
|
// 6c. Strip stale retry metadata from completed work items
|
|
1361
1294
|
cleaned.doneRetryCounts = 0;
|
|
1362
1295
|
for (const project of projects) {
|
|
1363
1296
|
try {
|
|
1364
|
-
|
|
1365
|
-
mutateWorkItems(wiPath, items => {
|
|
1297
|
+
mutateWorkItems(project, items => {
|
|
1366
1298
|
for (const item of items) {
|
|
1367
1299
|
if (item.status === shared.WI_STATUS.DONE && item._retryCount !== undefined) {
|
|
1368
1300
|
delete item._retryCount;
|
|
@@ -1373,8 +1305,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
1373
1305
|
} catch (e) { log('warn', 'cleanup done retry metadata: ' + e.message); }
|
|
1374
1306
|
}
|
|
1375
1307
|
try {
|
|
1376
|
-
|
|
1377
|
-
mutateWorkItems(centralPath, items => {
|
|
1308
|
+
mutateWorkItems('central', items => {
|
|
1378
1309
|
for (const item of items) {
|
|
1379
1310
|
if (item.status === shared.WI_STATUS.DONE && item._retryCount !== undefined) {
|
|
1380
1311
|
delete item._retryCount;
|
|
@@ -1396,15 +1327,14 @@ async function runCleanup(config, verbose = false) {
|
|
|
1396
1327
|
const prdStore = require('./prd-store');
|
|
1397
1328
|
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
1398
1329
|
for (const pf of prdFiles) {
|
|
1399
|
-
const prdPath = path.join(PRD_DIR, pf);
|
|
1400
1330
|
// Cheap out-of-transaction peek to skip PRDs that clearly need no work;
|
|
1401
1331
|
// the authoritative decision (and the write) happens inside mutatePrd.
|
|
1402
|
-
const peek = prdStore.readPrd(
|
|
1332
|
+
const peek = prdStore.readPrd(pf);
|
|
1403
1333
|
const needsMigration = peek?.missing_features?.some(feat =>
|
|
1404
1334
|
LEGACY_DONE_ALIASES.has(feat.status) || feat.status === LEGACY_NEEDS_REVIEW_STATUS);
|
|
1405
1335
|
if (!needsMigration) continue;
|
|
1406
1336
|
let migrated = 0;
|
|
1407
|
-
prdStore.mutatePrd(
|
|
1337
|
+
prdStore.mutatePrd(pf, (prd) => {
|
|
1408
1338
|
if (prd?.missing_features) {
|
|
1409
1339
|
for (const feat of prd.missing_features) {
|
|
1410
1340
|
if (LEGACY_DONE_ALIASES.has(feat.status)) {
|
|
@@ -1436,11 +1366,10 @@ async function runCleanup(config, verbose = false) {
|
|
|
1436
1366
|
const prdStore = require('./prd-store');
|
|
1437
1367
|
const orphanPrdEntries = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
1438
1368
|
for (const pf of orphanPrdEntries) {
|
|
1439
|
-
const
|
|
1440
|
-
const peek = prdStore.readPrd(prdPath);
|
|
1369
|
+
const peek = prdStore.readPrd(pf);
|
|
1441
1370
|
if (!peek?.missing_features) continue;
|
|
1442
1371
|
let reset = 0;
|
|
1443
|
-
prdStore.mutatePrd(
|
|
1372
|
+
prdStore.mutatePrd(pf, (prd) => {
|
|
1444
1373
|
if (prd?.missing_features) {
|
|
1445
1374
|
for (const feat of prd.missing_features) {
|
|
1446
1375
|
if ((feat.status === shared.WI_STATUS.DISPATCHED || feat.status === shared.WI_STATUS.FAILED) && !wiIds.has(feat.id)) {
|
|
@@ -1458,39 +1387,28 @@ async function runCleanup(config, verbose = false) {
|
|
|
1458
1387
|
}
|
|
1459
1388
|
} catch (e) { log('warn', 'orphan PRD status reset: ' + e.message); }
|
|
1460
1389
|
|
|
1461
|
-
// 10. CC tab sessions
|
|
1462
|
-
// user explicitly closes the tab (which fires DELETE /api/cc-sessions/:id).
|
|
1463
|
-
// Cleanup intentionally does NOT prune cc-sessions.json; doing so would
|
|
1464
|
-
// silently invalidate live chat tabs the user expects to keep.
|
|
1390
|
+
// 10. CC tab sessions persist until the user explicitly closes a tab.
|
|
1465
1391
|
cleaned.ccSessions = 0;
|
|
1466
1392
|
|
|
1467
1393
|
// 10b. Prune doc-chat sessions — cap at 100 entries, remove oldest beyond cap.
|
|
1468
|
-
// P-c2sess-1d8e: read+sort+write must run atomically under the file lock so a
|
|
1469
|
-
// concurrent dashboard persistDocSessions can't race the cap-trim.
|
|
1470
1394
|
cleaned.docSessions = 0;
|
|
1471
1395
|
try {
|
|
1472
|
-
const docSessionsPath = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
1473
1396
|
const DOC_SESSIONS_CAP = 100;
|
|
1474
|
-
|
|
1397
|
+
smallStateStore.applyDocSessionsMutation((docSessions) => {
|
|
1475
1398
|
if (!docSessions || typeof docSessions !== 'object' || Array.isArray(docSessions)) return docSessions;
|
|
1476
1399
|
const entries = Object.entries(docSessions);
|
|
1477
1400
|
if (entries.length <= DOC_SESSIONS_CAP) return docSessions;
|
|
1478
1401
|
entries.sort((a, b) => new Date(b.lastActiveAt || 0) - new Date(a.lastActiveAt || 0));
|
|
1479
1402
|
cleaned.docSessions = entries.length - DOC_SESSIONS_CAP;
|
|
1480
1403
|
return Object.fromEntries(entries.slice(0, DOC_SESSIONS_CAP));
|
|
1481
|
-
}
|
|
1482
|
-
} catch (e) { log('warn', 'prune doc
|
|
1404
|
+
});
|
|
1405
|
+
} catch (e) { log('warn', 'prune doc sessions: ' + e.message); }
|
|
1483
1406
|
|
|
1484
|
-
// 11. Cap
|
|
1485
|
-
// Also trim pendingContexts arrays to ENGINE_DEFAULTS.maxPendingContexts to prevent bloat.
|
|
1407
|
+
// 11. Cap cooldown state and pending context arrays.
|
|
1486
1408
|
cleaned.cooldowns = 0;
|
|
1487
1409
|
cleaned.pendingContextsTrimmed = 0;
|
|
1488
1410
|
try {
|
|
1489
|
-
const
|
|
1490
|
-
// safeJsonNoRestore — same rationale as engine/cooldown.js loadCooldowns:
|
|
1491
|
-
// resurrecting a stale .backup at cleanup time could re-introduce expired
|
|
1492
|
-
// entries the active code already pruned (P-bfa1d-safejson-no-restore).
|
|
1493
|
-
const cooldowns = safeJsonNoRestore(cooldownPath);
|
|
1411
|
+
const cooldowns = smallStateStore.readCooldowns();
|
|
1494
1412
|
if (cooldowns && typeof cooldowns === 'object') {
|
|
1495
1413
|
let dirty = false;
|
|
1496
1414
|
// Trim oversized pendingContexts arrays (one-time migration + ongoing cap)
|
|
@@ -1562,7 +1480,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
1562
1480
|
}
|
|
1563
1481
|
} catch { /* optional — file may not exist */ }
|
|
1564
1482
|
|
|
1565
|
-
// 14. Scrub stale temp agent keys from metrics
|
|
1483
|
+
// 14. Scrub stale temp agent keys from metrics state
|
|
1566
1484
|
try { scrubStaleMetrics(); } catch { /* best-effort cleanup */ }
|
|
1567
1485
|
|
|
1568
1486
|
// 15. Evict old completion reports — keep reports durable beyond the capped
|
|
@@ -1660,12 +1578,11 @@ async function runCleanup(config, verbose = false) {
|
|
|
1660
1578
|
|
|
1661
1579
|
// ─── Metrics Scrub ──────────────────────────────────────────────────────────
|
|
1662
1580
|
|
|
1663
|
-
/** Remove stale temp-*, agent1, and _test-* keys from metrics.
|
|
1581
|
+
/** Remove stale temp-*, agent1, and _test-* keys from metrics state.
|
|
1664
1582
|
* Mirrors the guard in lifecycle.js updateMetrics() — cleans up keys
|
|
1665
1583
|
* that were written before that guard existed. */
|
|
1666
1584
|
function scrubStaleMetrics() {
|
|
1667
|
-
|
|
1668
|
-
mutateJsonFileLocked(metricsPath, metrics => {
|
|
1585
|
+
mutateMetrics(metrics => {
|
|
1669
1586
|
for (const key of Object.keys(metrics)) {
|
|
1670
1587
|
if (key.startsWith('temp-') || key === 'agent1' || key.startsWith('_test')) {
|
|
1671
1588
|
delete metrics[key];
|