@yemi33/minions 0.1.2072 → 0.1.2074
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/docs/README.md +1 -0
- package/docs/auto-discovery.md +2 -1
- package/docs/kb-sweep.md +8 -0
- package/engine/db/migrations/007-watches.js +95 -0
- package/engine/db/migrations/008-small-state.js +155 -0
- package/engine/managed-spawn.js +7 -7
- package/engine/metrics-store.js +0 -0
- package/engine/pipeline.js +7 -9
- package/engine/pull-requests-store.js +30 -22
- package/engine/scheduler.js +6 -5
- package/engine/shared.js +101 -1
- package/engine/small-state-store.js +560 -0
- package/engine/watches-store.js +259 -0
- package/engine/watches.js +12 -16
- package/engine/work-items-store.js +33 -35
- package/engine/worktree-pool.js +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// engine/watches-store.js — SQL-backed implementation of engine/watches.json.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the Phase 2/3 work_items / pull_requests stores: a single
|
|
4
|
+
// table (no per-scope split — watches.json is a flat array, not
|
|
5
|
+
// per-project), diff-then-apply mutator, JSON dual-write mirror with
|
|
6
|
+
// (mtime, size) fingerprint for external-edit detection.
|
|
7
|
+
//
|
|
8
|
+
// readWatches() -> [watch, watch, ...]
|
|
9
|
+
// applyWatchesMutation(fn) -> diff-then-apply, returns { wrote, result }
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function _toMs(v) {
|
|
15
|
+
if (v == null) return null;
|
|
16
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
17
|
+
const parsed = Date.parse(v);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _watchesFilePath() {
|
|
22
|
+
const shared = require('./shared');
|
|
23
|
+
return path.join(shared.MINIONS_DIR, 'engine', 'watches.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _parseRow(row) {
|
|
27
|
+
if (!row || !row.data) return null;
|
|
28
|
+
try { return JSON.parse(row.data); }
|
|
29
|
+
catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _readJsonArrayFallback() {
|
|
33
|
+
const fp = _watchesFilePath();
|
|
34
|
+
let raw;
|
|
35
|
+
try { raw = fs.readFileSync(fp, 'utf8'); }
|
|
36
|
+
catch { return []; }
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
40
|
+
} catch (e) {
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.warn(`[watches-store] corrupt JSON in ${fp}: ${e.message}`);
|
|
44
|
+
} catch { /* console may be wrapped */ }
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Content-hash fingerprint — (mtime, size) is too coarse: same-size
|
|
50
|
+
// content swaps (e.g. last_checked timestamps that always have the same
|
|
51
|
+
// byte length) inside the same ms tick collide on both axes. Hashing the
|
|
52
|
+
// bytes is the unambiguous signal. SHA-1 on a few-KB file is sub-ms.
|
|
53
|
+
let _lastMirrorHash = null;
|
|
54
|
+
|
|
55
|
+
const crypto = require('crypto');
|
|
56
|
+
|
|
57
|
+
function _fileContentHash(filePath) {
|
|
58
|
+
try {
|
|
59
|
+
const buf = fs.readFileSync(filePath);
|
|
60
|
+
return crypto.createHash('sha1').update(buf).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _hydrateFromJson(db) {
|
|
66
|
+
const items = _readJsonArrayFallback();
|
|
67
|
+
db.prepare('DELETE FROM watches').run();
|
|
68
|
+
if (items.length === 0) return;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const ins = db.prepare(`
|
|
71
|
+
INSERT INTO watches (id, target, target_type, condition, status, owner, created_at, last_checked, last_triggered, data, updated_at)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(id) DO NOTHING
|
|
74
|
+
`);
|
|
75
|
+
for (const w of items) {
|
|
76
|
+
if (!w || !w.id) continue;
|
|
77
|
+
ins.run(
|
|
78
|
+
String(w.id),
|
|
79
|
+
typeof w.target === 'string' ? w.target : (w.target == null ? null : JSON.stringify(w.target)),
|
|
80
|
+
w.targetType || null,
|
|
81
|
+
w.condition || null,
|
|
82
|
+
String(w.status || 'active'),
|
|
83
|
+
w.owner || null,
|
|
84
|
+
_toMs(w.created_at),
|
|
85
|
+
_toMs(w.last_checked),
|
|
86
|
+
_toMs(w.last_triggered),
|
|
87
|
+
JSON.stringify(w),
|
|
88
|
+
now,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _resyncIfJsonDiverged(db) {
|
|
94
|
+
const jsonPath = _watchesFilePath();
|
|
95
|
+
const currentHash = _fileContentHash(jsonPath);
|
|
96
|
+
if (currentHash == null) return;
|
|
97
|
+
if (_lastMirrorHash != null && currentHash === _lastMirrorHash) return;
|
|
98
|
+
if (_lastMirrorHash == null) {
|
|
99
|
+
const sqlHas = db.prepare('SELECT 1 FROM watches LIMIT 1').get();
|
|
100
|
+
if (sqlHas) {
|
|
101
|
+
_lastMirrorHash = currentHash;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
_hydrateFromJson(db);
|
|
106
|
+
_lastMirrorHash = currentHash;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Read all watch rows from SQL only. No JSON fallback — used by the
|
|
110
|
+
// mirror writer to avoid resurrecting deleted rows: after the mutation
|
|
111
|
+
// deletes a watch from SQL but BEFORE the mirror lands, the JSON still
|
|
112
|
+
// holds the pre-mutation content, so a fallback read would round-trip
|
|
113
|
+
// the deleted row right back into JSON.
|
|
114
|
+
function _readWatchesFromSqlOnly(db) {
|
|
115
|
+
const rows = db.prepare('SELECT data FROM watches ORDER BY rowid').all();
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
const w = _parseRow(row);
|
|
119
|
+
if (w) out.push(w);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readWatches() {
|
|
125
|
+
const { getDb } = require('./db');
|
|
126
|
+
let db;
|
|
127
|
+
try { db = getDb(); }
|
|
128
|
+
catch { return _readJsonArrayFallback(); }
|
|
129
|
+
|
|
130
|
+
_resyncIfJsonDiverged(db);
|
|
131
|
+
|
|
132
|
+
const out = _readWatchesFromSqlOnly(db);
|
|
133
|
+
if (out.length === 0) {
|
|
134
|
+
// SQL is empty for first-time hydrate: trust the JSON if it has
|
|
135
|
+
// content. After hydrate ran, SQL is empty iff the user really has
|
|
136
|
+
// no watches.
|
|
137
|
+
const fallback = _readJsonArrayFallback();
|
|
138
|
+
if (fallback.length > 0) return fallback;
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _indexById(arr) {
|
|
145
|
+
const out = new Map();
|
|
146
|
+
for (const w of arr) {
|
|
147
|
+
if (!w || !w.id) continue;
|
|
148
|
+
out.set(String(w.id), w);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _computeWatchesDiff(before, after) {
|
|
154
|
+
const beforeMap = _indexById(before);
|
|
155
|
+
const afterMap = _indexById(after);
|
|
156
|
+
const toUpsert = [];
|
|
157
|
+
const toDelete = [];
|
|
158
|
+
for (const [id, w] of afterMap) {
|
|
159
|
+
const prev = beforeMap.get(id);
|
|
160
|
+
if (!prev) { toUpsert.push(w); continue; }
|
|
161
|
+
if (JSON.stringify(prev) !== JSON.stringify(w)) toUpsert.push(w);
|
|
162
|
+
}
|
|
163
|
+
for (const [id] of beforeMap) {
|
|
164
|
+
if (!afterMap.has(id)) toDelete.push(id);
|
|
165
|
+
}
|
|
166
|
+
return { toUpsert, toDelete };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _applyWatchesDiff(db, diff) {
|
|
170
|
+
if (diff.toUpsert.length === 0 && diff.toDelete.length === 0) return false;
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const upsertStmt = db.prepare(`
|
|
173
|
+
INSERT INTO watches (id, target, target_type, condition, status, owner, created_at, last_checked, last_triggered, data, updated_at)
|
|
174
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
175
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
176
|
+
target = excluded.target,
|
|
177
|
+
target_type = excluded.target_type,
|
|
178
|
+
condition = excluded.condition,
|
|
179
|
+
status = excluded.status,
|
|
180
|
+
owner = excluded.owner,
|
|
181
|
+
created_at = excluded.created_at,
|
|
182
|
+
last_checked = excluded.last_checked,
|
|
183
|
+
last_triggered = excluded.last_triggered,
|
|
184
|
+
data = excluded.data,
|
|
185
|
+
updated_at = excluded.updated_at
|
|
186
|
+
`);
|
|
187
|
+
const deleteStmt = db.prepare('DELETE FROM watches WHERE id = ?');
|
|
188
|
+
|
|
189
|
+
for (const w of diff.toUpsert) {
|
|
190
|
+
upsertStmt.run(
|
|
191
|
+
String(w.id),
|
|
192
|
+
typeof w.target === 'string' ? w.target : (w.target == null ? null : JSON.stringify(w.target)),
|
|
193
|
+
w.targetType || null,
|
|
194
|
+
w.condition || null,
|
|
195
|
+
String(w.status || 'active'),
|
|
196
|
+
w.owner || null,
|
|
197
|
+
_toMs(w.created_at),
|
|
198
|
+
_toMs(w.last_checked),
|
|
199
|
+
_toMs(w.last_triggered),
|
|
200
|
+
JSON.stringify(w),
|
|
201
|
+
now,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
for (const id of diff.toDelete) {
|
|
205
|
+
deleteStmt.run(id);
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyWatchesMutation(mutator) {
|
|
211
|
+
const { getDb, withTransaction } = require('./db');
|
|
212
|
+
let db;
|
|
213
|
+
try { db = getDb(); }
|
|
214
|
+
catch (e) {
|
|
215
|
+
throw new Error(`engine/watches-store: SQLite unavailable (${e.message}); cannot mutate watches`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return withTransaction(db, () => {
|
|
219
|
+
_resyncIfJsonDiverged(db);
|
|
220
|
+
const before = readWatches();
|
|
221
|
+
const beforeSnapshot = JSON.parse(JSON.stringify(before));
|
|
222
|
+
const next = mutator(before);
|
|
223
|
+
const after = (next === undefined || next === null)
|
|
224
|
+
? before
|
|
225
|
+
: (Array.isArray(next) ? next : before);
|
|
226
|
+
const diff = _computeWatchesDiff(beforeSnapshot, after);
|
|
227
|
+
const wrote = _applyWatchesDiff(db, diff);
|
|
228
|
+
return { wrote, result: after };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _mirrorJsonFromSql(filePath) {
|
|
233
|
+
try {
|
|
234
|
+
const shared = require('./shared');
|
|
235
|
+
const { getDb } = require('./db');
|
|
236
|
+
// Read SQL directly — bypass the JSON fallback so a deletion that
|
|
237
|
+
// leaves SQL empty doesn't resurrect from the stale JSON content.
|
|
238
|
+
const items = _readWatchesFromSqlOnly(getDb());
|
|
239
|
+
const target = filePath || _watchesFilePath();
|
|
240
|
+
shared.safeWrite(target, items);
|
|
241
|
+
const h = _fileContentHash(target);
|
|
242
|
+
if (h != null) _lastMirrorHash = h;
|
|
243
|
+
} catch {
|
|
244
|
+
// Mirror failures are non-fatal — SQL has already committed.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _resetWatchesTableForTest() {
|
|
249
|
+
const { getDb } = require('./db');
|
|
250
|
+
try { getDb().exec('DELETE FROM watches'); } catch { /* not initialized */ }
|
|
251
|
+
_lastMirrorHash = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
readWatches,
|
|
256
|
+
applyWatchesMutation,
|
|
257
|
+
_mirrorJsonFromSql,
|
|
258
|
+
_resetWatchesTableForTest,
|
|
259
|
+
};
|
package/engine/watches.js
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
const fs = require('fs');
|
|
68
68
|
const path = require('path');
|
|
69
69
|
const shared = require('./shared');
|
|
70
|
-
const { safeJsonArr, mutateJsonFileLocked, ts, uid, log, writeToInbox,
|
|
70
|
+
const { safeJsonArr, mutateJsonFileLocked, mutateWatches, ts, uid, log, writeToInbox,
|
|
71
71
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS } = shared;
|
|
72
72
|
const watchActions = require('./watch-actions');
|
|
73
73
|
|
|
@@ -230,11 +230,10 @@ function createWatch({ target, targetType, condition, interval, owner, descripti
|
|
|
230
230
|
_history: [],
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
if (!Array.isArray(watches)) watches = [];
|
|
233
|
+
mutateWatches((watches) => {
|
|
235
234
|
watches.push(watch);
|
|
236
235
|
return watches;
|
|
237
|
-
}
|
|
236
|
+
});
|
|
238
237
|
|
|
239
238
|
log('info', `Watch created: ${watch.id} → ${watch.target} (${watch.condition})`);
|
|
240
239
|
return watch;
|
|
@@ -264,8 +263,7 @@ function updateWatch(id, updates) {
|
|
|
264
263
|
if (reqErr) throw new Error(reqErr);
|
|
265
264
|
}
|
|
266
265
|
let found = null;
|
|
267
|
-
|
|
268
|
-
if (!Array.isArray(watches)) return watches;
|
|
266
|
+
mutateWatches((watches) => {
|
|
269
267
|
const watch = watches.find(w => w.id === id);
|
|
270
268
|
if (!watch) return watches;
|
|
271
269
|
// Only allow safe field updates
|
|
@@ -275,7 +273,7 @@ function updateWatch(id, updates) {
|
|
|
275
273
|
}
|
|
276
274
|
found = { ...watch };
|
|
277
275
|
return watches;
|
|
278
|
-
}
|
|
276
|
+
});
|
|
279
277
|
return found;
|
|
280
278
|
}
|
|
281
279
|
|
|
@@ -287,15 +285,14 @@ function updateWatch(id, updates) {
|
|
|
287
285
|
function deleteWatch(id) {
|
|
288
286
|
if (!id) return false;
|
|
289
287
|
let deleted = false;
|
|
290
|
-
|
|
291
|
-
if (!Array.isArray(watches)) return watches;
|
|
288
|
+
mutateWatches((watches) => {
|
|
292
289
|
const idx = watches.findIndex(w => w.id === id);
|
|
293
290
|
if (idx >= 0) {
|
|
294
291
|
watches.splice(idx, 1);
|
|
295
292
|
deleted = true;
|
|
296
293
|
}
|
|
297
294
|
return watches;
|
|
298
|
-
}
|
|
295
|
+
});
|
|
299
296
|
if (deleted) log('info', `Watch deleted: ${id}`);
|
|
300
297
|
return deleted;
|
|
301
298
|
}
|
|
@@ -413,8 +410,8 @@ function checkWatches(config, state) {
|
|
|
413
410
|
const notifications = [];
|
|
414
411
|
const actionsToRun = [];
|
|
415
412
|
|
|
416
|
-
|
|
417
|
-
if (
|
|
413
|
+
mutateWatches((watches) => {
|
|
414
|
+
if (watches.length === 0) return watches;
|
|
418
415
|
|
|
419
416
|
for (const watch of watches) {
|
|
420
417
|
try {
|
|
@@ -539,7 +536,7 @@ function checkWatches(config, state) {
|
|
|
539
536
|
}
|
|
540
537
|
|
|
541
538
|
return watches;
|
|
542
|
-
}
|
|
539
|
+
});
|
|
543
540
|
|
|
544
541
|
// Fire notifications outside the lock — writeToInbox does disk I/O
|
|
545
542
|
for (const n of notifications) {
|
|
@@ -591,8 +588,7 @@ async function _runActionTask(task) {
|
|
|
591
588
|
// can inspect each step's result. Single-action watches keep the legacy
|
|
592
589
|
// `dispatchedItemId` shortcut for back-compat.
|
|
593
590
|
try {
|
|
594
|
-
|
|
595
|
-
if (!Array.isArray(watches)) return watches;
|
|
591
|
+
mutateWatches((watches) => {
|
|
596
592
|
const w = watches.find(x => x.id === task.watchId);
|
|
597
593
|
if (w) {
|
|
598
594
|
if (isChain) {
|
|
@@ -639,7 +635,7 @@ async function _runActionTask(task) {
|
|
|
639
635
|
}
|
|
640
636
|
}
|
|
641
637
|
return watches;
|
|
642
|
-
}
|
|
638
|
+
});
|
|
643
639
|
} catch (err) {
|
|
644
640
|
log('warn', `Watch ${task.watchId} action result persist failed: ${err.message}`);
|
|
645
641
|
}
|
|
@@ -116,28 +116,19 @@ function readWorkItemsForScope(scope) {
|
|
|
116
116
|
// scope is bounded by the JSON's row count.
|
|
117
117
|
function _resyncScopeIfJsonDiverged(db, scope) {
|
|
118
118
|
const jsonPath = _filePathForScope(scope);
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// mtime-only checks miss on Windows (ms-rounded mtimeMs collides).
|
|
125
|
-
// Resync is idempotent (DELETE + bulk re-INSERT from JSON).
|
|
126
|
-
if (lastMirror != null
|
|
127
|
-
&& current.mtime === lastMirror.mtime
|
|
128
|
-
&& current.size === lastMirror.size) return;
|
|
129
|
-
// No mirror record but JSON exists: first-time hydrate iff SQL is empty
|
|
130
|
-
// for this scope (avoid trampling a freshly-backfilled migration state
|
|
131
|
-
// on the very first read).
|
|
132
|
-
if (lastMirror == null) {
|
|
119
|
+
const currentHash = _fileContentHash(jsonPath);
|
|
120
|
+
const lastHash = _lastMirrorHashByScope.get(scope);
|
|
121
|
+
if (currentHash == null) return;
|
|
122
|
+
if (lastHash != null && currentHash === lastHash) return;
|
|
123
|
+
if (lastHash == null) {
|
|
133
124
|
const sqlHas = db.prepare('SELECT 1 FROM work_items WHERE scope = ? LIMIT 1').get(scope);
|
|
134
125
|
if (sqlHas) {
|
|
135
|
-
|
|
126
|
+
_lastMirrorHashByScope.set(scope, currentHash);
|
|
136
127
|
return;
|
|
137
128
|
}
|
|
138
129
|
}
|
|
139
130
|
_hydrateScopeFromJson(db, scope);
|
|
140
|
-
|
|
131
|
+
_lastMirrorHashByScope.set(scope, currentHash);
|
|
141
132
|
}
|
|
142
133
|
|
|
143
134
|
// Read all rows across all scopes — used by queries.getWorkItems which
|
|
@@ -243,18 +234,18 @@ function _applyWorkItemsDiff(db, diff) {
|
|
|
243
234
|
// wrote — true iff at least one INSERT/UPDATE/DELETE landed
|
|
244
235
|
// result — the post-mutation array (legacy return shape of
|
|
245
236
|
// mutateJsonFileLocked)
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
// — content swaps almost always change byte length.
|
|
252
|
-
const _lastMirrorByScope = new Map();
|
|
237
|
+
// Content-hash fingerprint of the JSON mirror per scope, so external
|
|
238
|
+
// edits are detected unambiguously. (mtime, size) is too coarse: same-
|
|
239
|
+
// length timestamp swaps (last_checked / last_triggered / completed_at)
|
|
240
|
+
// preserve both axes. SHA-1 is sub-ms on a few-hundred-KB file.
|
|
241
|
+
const _lastMirrorHashByScope = new Map();
|
|
253
242
|
|
|
254
|
-
|
|
243
|
+
const crypto = require('crypto');
|
|
244
|
+
|
|
245
|
+
function _fileContentHash(filePath) {
|
|
255
246
|
try {
|
|
256
|
-
const
|
|
257
|
-
return
|
|
247
|
+
const buf = fs.readFileSync(filePath);
|
|
248
|
+
return crypto.createHash('sha1').update(buf).digest('hex');
|
|
258
249
|
}
|
|
259
250
|
catch { return null; }
|
|
260
251
|
}
|
|
@@ -325,17 +316,24 @@ function applyWorkItemsMutation(scope, mutator) {
|
|
|
325
316
|
function _mirrorJsonFromSql(scope, filePath) {
|
|
326
317
|
try {
|
|
327
318
|
const shared = require('./shared');
|
|
328
|
-
const
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
319
|
+
const { getDb } = require('./db');
|
|
320
|
+
// Read SQL directly for this scope — bypass the JSON fallback so a
|
|
321
|
+
// mutation that empties SQL for the scope doesn't resurrect stale
|
|
322
|
+
// JSON content. (See pull-requests-store and watches-store for the
|
|
323
|
+
// same defense.)
|
|
324
|
+
const db = getDb();
|
|
325
|
+
const rows = db.prepare('SELECT data FROM work_items WHERE scope = ? ORDER BY rowid').all(scope);
|
|
326
|
+
const items = [];
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
const wi = _parseRow(row);
|
|
329
|
+
if (!wi) continue;
|
|
330
|
+
if (wi._source) delete wi._source;
|
|
331
|
+
items.push(wi);
|
|
332
|
+
}
|
|
332
333
|
const target = filePath || _filePathForScope(scope);
|
|
333
334
|
shared.safeWrite(target, items);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// catches them.
|
|
337
|
-
const fp = _statFingerprint(target);
|
|
338
|
-
if (fp != null) _lastMirrorByScope.set(scope, fp);
|
|
335
|
+
const h = _fileContentHash(target);
|
|
336
|
+
if (h != null) _lastMirrorHashByScope.set(scope, h);
|
|
339
337
|
} catch {
|
|
340
338
|
// Mirror failures are non-fatal: SQL has already committed.
|
|
341
339
|
}
|
package/engine/worktree-pool.js
CHANGED
|
@@ -61,12 +61,16 @@ function readPool() {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function mutateWorktreePool(mutator) {
|
|
64
|
-
|
|
64
|
+
// Phase 7: route through shared.mutateWorktreePool (SQL-backed store +
|
|
65
|
+
// JSON mirror). Defensive wrappers around `data.entries` kept for the
|
|
66
|
+
// legacy fallback path inside the helper (when SQLite is unavailable
|
|
67
|
+
// and we fall through to mutateJsonFileLocked).
|
|
68
|
+
return shared.mutateWorktreePool((data) => {
|
|
65
69
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = { entries: [] };
|
|
66
70
|
if (!Array.isArray(data.entries)) data.entries = [];
|
|
67
71
|
const next = mutator(data);
|
|
68
72
|
return next === undefined ? data : next;
|
|
69
|
-
}
|
|
73
|
+
});
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2074",
|
|
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"
|