@yemi33/minions 0.1.2114 → 0.1.2115
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/engine/db/migrations/010-pr-links.js +61 -0
- package/engine/shared.js +39 -8
- package/engine/small-state-store.js +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// engine/db/migrations/010-pr-links.js
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.1: move `engine/pr-links.json` into SQL.
|
|
4
|
+
//
|
|
5
|
+
// pr-links.json is the *fallback* PR → work-item linkage store. The
|
|
6
|
+
// primary source is `pull_requests.prdItems[]` (managed via the
|
|
7
|
+
// pull-requests-store). Stray PR IDs that aren't covered by a project
|
|
8
|
+
// pull-requests record still need to be discoverable — that's what this
|
|
9
|
+
// table holds.
|
|
10
|
+
//
|
|
11
|
+
// Shape: { [prId]: <value> } where <value> is typically an array of item IDs
|
|
12
|
+
// but may also be a bare string from legacy writes. Readers normalize.
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
function _resolveMinionsDir() {
|
|
18
|
+
const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
|
|
19
|
+
if (envHome) return envHome;
|
|
20
|
+
try { return require('../../shared').MINIONS_DIR; } catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _readJsonOr(filePath, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch { return fallback; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
version: 10,
|
|
32
|
+
description: 'pr_links: schema + pr-links.json backfill (fallback static store)',
|
|
33
|
+
up(db) {
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE pr_links (
|
|
36
|
+
pr_id TEXT PRIMARY KEY,
|
|
37
|
+
data TEXT NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
const minionsDir = _resolveMinionsDir();
|
|
43
|
+
if (!minionsDir) return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
let inserted = 0;
|
|
46
|
+
|
|
47
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'pr-links.json'), null);
|
|
48
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
49
|
+
const ins = db.prepare(`INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?)`);
|
|
50
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
51
|
+
try {
|
|
52
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
53
|
+
inserted += 1;
|
|
54
|
+
} catch { /* duplicate / corrupt — skip */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.log(`[db-migrate] v10: backfilled ${inserted} pr-links rows; pr-links.json kept as dual-write mirror`);
|
|
60
|
+
},
|
|
61
|
+
};
|
package/engine/shared.js
CHANGED
|
@@ -532,6 +532,10 @@ function _routeJsonReadToSql(p) {
|
|
|
532
532
|
const store = require('./small-state-store');
|
|
533
533
|
return { value: store.readQaSessions() };
|
|
534
534
|
}
|
|
535
|
+
if (norm.endsWith('/engine/pr-links.json')) {
|
|
536
|
+
const store = require('./small-state-store');
|
|
537
|
+
return { value: store.readPrLinks() };
|
|
538
|
+
}
|
|
535
539
|
// Per-project work-items.json — match `/projects/<name>/work-items.json`.
|
|
536
540
|
// When SQL has no rows for the scope AND the JSON file is absent on
|
|
537
541
|
// disk, preserve the legacy "file missing → null" semantic. This guards
|
|
@@ -582,9 +586,9 @@ function _routeJsonReadToSql(p) {
|
|
|
582
586
|
*
|
|
583
587
|
* **SQL routing (Phase 9):** when the path matches a migrated state file
|
|
584
588
|
* (work-items, pull-requests, dispatch, metrics, watches, schedule-runs,
|
|
585
|
-
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions
|
|
586
|
-
* the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
587
|
-
* on disk is no longer authoritative.
|
|
589
|
+
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions,
|
|
590
|
+
* pr-links), the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
591
|
+
* JSON file on disk is no longer authoritative.
|
|
588
592
|
*
|
|
589
593
|
* Counterpart: `safeJsonNoRestore` for terminal artifacts and "missing == gone"
|
|
590
594
|
* reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
|
|
@@ -4988,9 +4992,19 @@ function getPrLinks() {
|
|
|
4988
4992
|
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
4989
4993
|
}
|
|
4990
4994
|
} catch { /* SQL unavailable — skip primary source */ }
|
|
4991
|
-
// Fallback: static pr-links.json
|
|
4995
|
+
// Fallback: pr_links SQL store (was static pr-links.json file). Phase 9.1
|
|
4996
|
+
// routes both reads and writes through small-state-store; the JSON file is
|
|
4997
|
+
// kept as a dual-write mirror. SQL failures fall through to the JSON file.
|
|
4998
|
+
let static_ = null;
|
|
4992
4999
|
try {
|
|
4993
|
-
const
|
|
5000
|
+
const store = require('./small-state-store');
|
|
5001
|
+
static_ = store.readPrLinks();
|
|
5002
|
+
} catch { static_ = null; }
|
|
5003
|
+
if (!static_ || Object.keys(static_).length === 0) {
|
|
5004
|
+
try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
|
|
5005
|
+
catch { static_ = null; }
|
|
5006
|
+
}
|
|
5007
|
+
if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
|
|
4994
5008
|
for (const [k, v] of Object.entries(static_)) {
|
|
4995
5009
|
const canonical = parseCanonicalPrId(k);
|
|
4996
5010
|
let normalizedKey = canonical ? `${canonical.scope}#${canonical.prNumber}` : k;
|
|
@@ -5004,7 +5018,7 @@ function getPrLinks() {
|
|
|
5004
5018
|
}
|
|
5005
5019
|
if (!links[normalizedKey]) mergePrLinkItems(links, normalizedKey, v);
|
|
5006
5020
|
}
|
|
5007
|
-
}
|
|
5021
|
+
}
|
|
5008
5022
|
return links;
|
|
5009
5023
|
}
|
|
5010
5024
|
|
|
@@ -5030,7 +5044,7 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5030
5044
|
const effectivePrId = canonicalPrId || prId;
|
|
5031
5045
|
if (!effectivePrId || !itemId) return;
|
|
5032
5046
|
const legacyPrId = String(prId || '');
|
|
5033
|
-
|
|
5047
|
+
const mutator = (links) => {
|
|
5034
5048
|
if (!links || Array.isArray(links) || typeof links !== 'object') links = {};
|
|
5035
5049
|
const mergedCurrent = new Set(normalizePrLinkItems(links[effectivePrId]));
|
|
5036
5050
|
if (legacyPrId && legacyPrId !== effectivePrId && links[legacyPrId]) {
|
|
@@ -5040,7 +5054,24 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5040
5054
|
if (!mergedCurrent.has(itemId)) mergedCurrent.add(itemId);
|
|
5041
5055
|
links[effectivePrId] = [...mergedCurrent];
|
|
5042
5056
|
return links;
|
|
5043
|
-
}
|
|
5057
|
+
};
|
|
5058
|
+
// Phase 9.1: pr-links is SQL-canonical via small-state-store; the JSON file
|
|
5059
|
+
// is a dual-write mirror. SQLite failures fall through to the legacy JSON
|
|
5060
|
+
// path so older installs without --experimental-sqlite still work.
|
|
5061
|
+
let routedViaSql = false;
|
|
5062
|
+
try {
|
|
5063
|
+
const store = require('./small-state-store');
|
|
5064
|
+
store.applyPrLinksMutation(mutator);
|
|
5065
|
+
try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
|
|
5066
|
+
routedViaSql = true;
|
|
5067
|
+
} catch (e) {
|
|
5068
|
+
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5069
|
+
throw e;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
if (!routedViaSql) {
|
|
5073
|
+
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
5074
|
+
}
|
|
5044
5075
|
|
|
5045
5076
|
if (!project) return;
|
|
5046
5077
|
const prPath = projectPrPath(project);
|
|
@@ -829,6 +829,118 @@ function _mirrorQaSessionsJson(filePath) {
|
|
|
829
829
|
} catch { /* mirror best-effort */ }
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
// ─── pr_links ───────────────────────────────────────────────────────────────
|
|
833
|
+
// Shape: { [prId]: <value> } where <value> is typically array of item IDs
|
|
834
|
+
// but may also be a bare string (legacy). Readers normalize.
|
|
835
|
+
|
|
836
|
+
let _prLinksHash = null;
|
|
837
|
+
|
|
838
|
+
function _hydratePrLinks(db) {
|
|
839
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
840
|
+
const raw = _readJson(fp) || {};
|
|
841
|
+
db.prepare('DELETE FROM pr_links').run();
|
|
842
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
const ins = db.prepare('INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(pr_id) DO NOTHING');
|
|
845
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
846
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function _resyncPrLinksIfDiverged(db) {
|
|
851
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
852
|
+
const currentHash = _fileContentHash(fp);
|
|
853
|
+
if (currentHash == null) return;
|
|
854
|
+
if (_prLinksHash != null && currentHash === _prLinksHash) return;
|
|
855
|
+
if (_prLinksHash == null) {
|
|
856
|
+
const sqlHas = db.prepare('SELECT 1 FROM pr_links LIMIT 1').get();
|
|
857
|
+
if (sqlHas) {
|
|
858
|
+
_prLinksHash = currentHash;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
_hydratePrLinks(db);
|
|
863
|
+
_prLinksHash = currentHash;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function _readPrLinksFromSql(db) {
|
|
867
|
+
const rows = db.prepare('SELECT pr_id, data FROM pr_links').all();
|
|
868
|
+
const out = {};
|
|
869
|
+
for (const row of rows) {
|
|
870
|
+
try { out[row.pr_id] = JSON.parse(row.data); }
|
|
871
|
+
catch { /* skip malformed */ }
|
|
872
|
+
}
|
|
873
|
+
return out;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readPrLinks() {
|
|
877
|
+
const { getDb } = require('./db');
|
|
878
|
+
let db;
|
|
879
|
+
try { db = getDb(); }
|
|
880
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
881
|
+
try { _resyncPrLinksIfDiverged(db); }
|
|
882
|
+
catch { /* table may be missing on a stale install — fall back to JSON */ }
|
|
883
|
+
let out;
|
|
884
|
+
try { out = _readPrLinksFromSql(db); }
|
|
885
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
886
|
+
if (Object.keys(out).length === 0) {
|
|
887
|
+
const fallback = _readJson(_resolveFilePath('pr-links.json'));
|
|
888
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function applyPrLinksMutation(mutator) {
|
|
895
|
+
const { getDb, withTransaction } = require('./db');
|
|
896
|
+
let db;
|
|
897
|
+
try { db = getDb(); }
|
|
898
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
899
|
+
|
|
900
|
+
return withTransaction(db, () => {
|
|
901
|
+
_resyncPrLinksIfDiverged(db);
|
|
902
|
+
const before = _readPrLinksFromSql(db);
|
|
903
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
904
|
+
const next = mutator(before);
|
|
905
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
906
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
907
|
+
return { wrote: false, result: beforeSnap };
|
|
908
|
+
}
|
|
909
|
+
const afterIds = new Set(Object.keys(after));
|
|
910
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
911
|
+
let wrote = false;
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const upsert = db.prepare(`
|
|
914
|
+
INSERT INTO pr_links (pr_id, data, updated_at)
|
|
915
|
+
VALUES (?, ?, ?)
|
|
916
|
+
ON CONFLICT(pr_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
917
|
+
`);
|
|
918
|
+
const del = db.prepare('DELETE FROM pr_links WHERE pr_id = ?');
|
|
919
|
+
for (const id of afterIds) {
|
|
920
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
921
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
922
|
+
wrote = true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (const id of beforeIds) {
|
|
926
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
927
|
+
}
|
|
928
|
+
return { wrote, result: after };
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function _mirrorPrLinksJson(filePath) {
|
|
933
|
+
try {
|
|
934
|
+
const shared = require('./shared');
|
|
935
|
+
const { getDb } = require('./db');
|
|
936
|
+
const obj = _readPrLinksFromSql(getDb());
|
|
937
|
+
const target = filePath || _resolveFilePath('pr-links.json');
|
|
938
|
+
shared.safeWrite(target, obj);
|
|
939
|
+
const h = _fileContentHash(target);
|
|
940
|
+
if (h != null) _prLinksHash = h;
|
|
941
|
+
} catch { /* mirror best-effort */ }
|
|
942
|
+
}
|
|
943
|
+
|
|
832
944
|
// ─── Test seam ─────────────────────────────────────────────────────────────
|
|
833
945
|
|
|
834
946
|
function _resetAllForTest() {
|
|
@@ -841,6 +953,7 @@ function _resetAllForTest() {
|
|
|
841
953
|
db.exec('DELETE FROM worktree_pool');
|
|
842
954
|
try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
|
|
843
955
|
try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
|
|
956
|
+
try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
|
|
844
957
|
} catch { /* not initialized */ }
|
|
845
958
|
_scheduleRunsHash = null;
|
|
846
959
|
_pipelineRunsHash = null;
|
|
@@ -848,6 +961,7 @@ function _resetAllForTest() {
|
|
|
848
961
|
_worktreePoolHash = null;
|
|
849
962
|
_qaRunsHash = null;
|
|
850
963
|
_qaSessionsHash = null;
|
|
964
|
+
_prLinksHash = null;
|
|
851
965
|
}
|
|
852
966
|
|
|
853
967
|
module.exports = {
|
|
@@ -875,6 +989,10 @@ module.exports = {
|
|
|
875
989
|
readQaSessions,
|
|
876
990
|
applyQaSessionsMutation,
|
|
877
991
|
_mirrorQaSessionsJson,
|
|
992
|
+
// pr_links
|
|
993
|
+
readPrLinks,
|
|
994
|
+
applyPrLinksMutation,
|
|
995
|
+
_mirrorPrLinksJson,
|
|
878
996
|
// test seam
|
|
879
997
|
_resetAllForTest,
|
|
880
998
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2115",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|