@yemi33/minions 0.1.2379 → 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/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/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 +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/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 +147 -343
- 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
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
// { wrote, result }.
|
|
8
8
|
//
|
|
9
9
|
// `scope` is 'central' or a project name.
|
|
10
|
-
//
|
|
11
|
-
// All callers go through shared.mutateWorkItems and
|
|
12
|
-
// queries.getWorkItems — those are thin wrappers over the helpers here.
|
|
13
10
|
|
|
14
11
|
function _toMs(v) {
|
|
15
12
|
if (v == null) return null;
|
|
@@ -18,21 +15,6 @@ function _toMs(v) {
|
|
|
18
15
|
return Number.isFinite(parsed) ? parsed : null;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
// Map an absolute filePath → scope key. The store doesn't know about the
|
|
22
|
-
// caller's file layout; we derive scope from the path's last two segments
|
|
23
|
-
// because every work-items file in this codebase sits at either
|
|
24
|
-
// <root>/work-items.json (central) or <root>/projects/<name>/work-items.json.
|
|
25
|
-
function scopeForFilePath(filePath) {
|
|
26
|
-
const norm = String(filePath).replace(/\\/g, '/');
|
|
27
|
-
const m = norm.match(/projects\/([^/]+)\/work-items\.json$/);
|
|
28
|
-
if (m) return m[1];
|
|
29
|
-
// Anything else — including the central work-items.json under MINIONS_DIR
|
|
30
|
-
// and any one-off test fixture file — lands in 'central'. Tests that
|
|
31
|
-
// need to keep multiple fixture files distinct should put them under a
|
|
32
|
-
// synthetic projects/<name>/ subtree.
|
|
33
|
-
return 'central';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
18
|
function _parseRow(row) {
|
|
37
19
|
if (!row || !row.data) return null;
|
|
38
20
|
try { return JSON.parse(row.data); }
|
|
@@ -45,7 +27,7 @@ function readWorkItemsForScope(scope) {
|
|
|
45
27
|
|
|
46
28
|
const rows = db.prepare(`
|
|
47
29
|
SELECT data FROM work_items
|
|
48
|
-
WHERE scope = ?
|
|
30
|
+
WHERE scope = ? AND archived = 0
|
|
49
31
|
ORDER BY rowid
|
|
50
32
|
`).all(scope);
|
|
51
33
|
|
|
@@ -64,7 +46,7 @@ function readAllWorkItems() {
|
|
|
64
46
|
const { getDb } = require('./db');
|
|
65
47
|
const db = getDb();
|
|
66
48
|
|
|
67
|
-
const rows = db.prepare('SELECT data, scope FROM work_items ORDER BY rowid').all();
|
|
49
|
+
const rows = db.prepare('SELECT data, scope FROM work_items WHERE archived = 0 ORDER BY rowid').all();
|
|
68
50
|
const out = [];
|
|
69
51
|
for (const row of rows) {
|
|
70
52
|
const wi = _parseRow(row);
|
|
@@ -85,14 +67,18 @@ function _indexById(arr) {
|
|
|
85
67
|
return out;
|
|
86
68
|
}
|
|
87
69
|
|
|
88
|
-
function _computeWorkItemsDiff(scope, before, after) {
|
|
70
|
+
function _computeWorkItemsDiff(scope, before, after, { allowIncomplete = false } = {}) {
|
|
89
71
|
const beforeMap = _indexById(before);
|
|
90
72
|
const afterMap = _indexById(after);
|
|
91
73
|
const toUpsert = [];
|
|
92
74
|
const toDelete = [];
|
|
93
75
|
for (const [id, wi] of afterMap) {
|
|
94
76
|
const prev = beforeMap.get(id);
|
|
95
|
-
if (!prev) {
|
|
77
|
+
if (!prev) {
|
|
78
|
+
if (!allowIncomplete) require('./work-item-validation').validateNewWorkItem(wi);
|
|
79
|
+
toUpsert.push(wi);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
96
82
|
if (JSON.stringify(prev) !== JSON.stringify(wi)) toUpsert.push(wi);
|
|
97
83
|
}
|
|
98
84
|
for (const [id] of beforeMap) {
|
|
@@ -105,8 +91,8 @@ function _applyWorkItemsDiff(db, diff) {
|
|
|
105
91
|
if (diff.toUpsert.length === 0 && diff.toDelete.length === 0) return false;
|
|
106
92
|
const now = Date.now();
|
|
107
93
|
const upsertStmt = db.prepare(`
|
|
108
|
-
INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at)
|
|
109
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
94
|
+
INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at, archived)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
110
96
|
ON CONFLICT(scope, id) DO UPDATE SET
|
|
111
97
|
status = excluded.status,
|
|
112
98
|
type = excluded.type,
|
|
@@ -115,9 +101,10 @@ function _applyWorkItemsDiff(db, diff) {
|
|
|
115
101
|
created_at = excluded.created_at,
|
|
116
102
|
completed_at = excluded.completed_at,
|
|
117
103
|
data = excluded.data,
|
|
118
|
-
updated_at = excluded.updated_at
|
|
104
|
+
updated_at = excluded.updated_at,
|
|
105
|
+
archived = 0
|
|
119
106
|
`);
|
|
120
|
-
const deleteStmt = db.prepare('DELETE FROM work_items WHERE scope = ? AND id = ?');
|
|
107
|
+
const deleteStmt = db.prepare('DELETE FROM work_items WHERE scope = ? AND id = ? AND archived = 0');
|
|
121
108
|
|
|
122
109
|
for (const wi of diff.toUpsert) {
|
|
123
110
|
upsertStmt.run(
|
|
@@ -147,7 +134,7 @@ function _applyWorkItemsDiff(db, diff) {
|
|
|
147
134
|
// wrote — true iff at least one INSERT/UPDATE/DELETE landed
|
|
148
135
|
// result — the post-mutation array (legacy return shape of
|
|
149
136
|
// mutateJsonFileLocked)
|
|
150
|
-
function applyWorkItemsMutation(scope, mutator) {
|
|
137
|
+
function applyWorkItemsMutation(scope, mutator, opts) {
|
|
151
138
|
const { getDb, withTransaction } = require('./db');
|
|
152
139
|
const db = getDb();
|
|
153
140
|
|
|
@@ -162,7 +149,7 @@ function applyWorkItemsMutation(scope, mutator) {
|
|
|
162
149
|
const after = (next === undefined || next === null)
|
|
163
150
|
? before
|
|
164
151
|
: (Array.isArray(next) ? next : before);
|
|
165
|
-
const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after);
|
|
152
|
+
const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after, opts);
|
|
166
153
|
const wrote = _applyWorkItemsDiff(db, diff);
|
|
167
154
|
return { wrote, result: after };
|
|
168
155
|
});
|
|
@@ -185,10 +172,121 @@ function dropScope(scope) {
|
|
|
185
172
|
} catch { /* db unavailable */ }
|
|
186
173
|
}
|
|
187
174
|
|
|
175
|
+
function readArchivedWorkItemsForScope(scope) {
|
|
176
|
+
const rows = require('./db').getDb().prepare(`
|
|
177
|
+
SELECT data FROM work_items
|
|
178
|
+
WHERE scope = ? AND archived = 1
|
|
179
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
180
|
+
`).all(scope);
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const row of rows) {
|
|
183
|
+
const item = _parseRow(row);
|
|
184
|
+
if (item) out.push(item);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function archiveWorkItem(scope, id) {
|
|
190
|
+
const { getDb, withTransaction } = require('./db');
|
|
191
|
+
const db = getDb();
|
|
192
|
+
const archived = withTransaction(db, () => {
|
|
193
|
+
const row = db.prepare('SELECT data FROM work_items WHERE scope=? AND id=? AND archived=0').get(scope, String(id));
|
|
194
|
+
if (!row) return null;
|
|
195
|
+
const item = _parseRow(row);
|
|
196
|
+
if (!item) throw new Error(`work item ${id} has invalid SQL data`);
|
|
197
|
+
item._archived = true;
|
|
198
|
+
item.archivedAt = item.archivedAt || new Date().toISOString();
|
|
199
|
+
db.prepare('UPDATE work_items SET archived=1, data=?, updated_at=? WHERE scope=? AND id=?')
|
|
200
|
+
.run(JSON.stringify(item), Date.now(), scope, String(id));
|
|
201
|
+
return item;
|
|
202
|
+
});
|
|
203
|
+
if (archived) {
|
|
204
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
205
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
206
|
+
}
|
|
207
|
+
return archived;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function replaceArchivedWorkItems(scope, items) {
|
|
211
|
+
const { getDb, withTransaction } = require('./db');
|
|
212
|
+
const db = getDb();
|
|
213
|
+
const count = withTransaction(db, () => {
|
|
214
|
+
db.prepare('DELETE FROM work_items WHERE scope=? AND archived=1').run(scope);
|
|
215
|
+
const insert = db.prepare(`
|
|
216
|
+
INSERT OR IGNORE INTO work_items
|
|
217
|
+
(id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at, archived)
|
|
218
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
219
|
+
`);
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
let inserted = 0;
|
|
222
|
+
for (const raw of Array.isArray(items) ? items : []) {
|
|
223
|
+
if (!raw || typeof raw !== 'object' || !raw.id) continue;
|
|
224
|
+
const item = { ...raw, _archived: true };
|
|
225
|
+
item.archivedAt = item.archivedAt || new Date(now).toISOString();
|
|
226
|
+
const result = insert.run(
|
|
227
|
+
String(item.id),
|
|
228
|
+
scope,
|
|
229
|
+
String(item.status || 'done'),
|
|
230
|
+
item.type || null,
|
|
231
|
+
item.dispatched_to || item.agent || null,
|
|
232
|
+
item.parent_id || null,
|
|
233
|
+
_toMs(item.created_at || item.created),
|
|
234
|
+
_toMs(item.completed_at || item.completedAt),
|
|
235
|
+
JSON.stringify(item),
|
|
236
|
+
now,
|
|
237
|
+
);
|
|
238
|
+
inserted += Number(result.changes) || 0;
|
|
239
|
+
}
|
|
240
|
+
return inserted;
|
|
241
|
+
});
|
|
242
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
243
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
244
|
+
return count;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function moveWorkItem(sourceScope, targetScope, id, item) {
|
|
248
|
+
const { getDb, withTransaction } = require('./db');
|
|
249
|
+
const db = getDb();
|
|
250
|
+
const moved = withTransaction(db, () => {
|
|
251
|
+
const source = db.prepare('SELECT 1 FROM work_items WHERE scope=? AND id=? AND archived=0')
|
|
252
|
+
.get(sourceScope, String(id));
|
|
253
|
+
if (!source) return false;
|
|
254
|
+
const target = db.prepare('SELECT 1 FROM work_items WHERE scope=? AND id=?').get(targetScope, String(id));
|
|
255
|
+
if (target) throw new Error(`work item ${id} already exists in scope ${targetScope}`);
|
|
256
|
+
const result = db.prepare(`
|
|
257
|
+
UPDATE work_items SET
|
|
258
|
+
scope=?, status=?, type=?, agent=?, parent_id=?, created_at=?,
|
|
259
|
+
completed_at=?, data=?, updated_at=?
|
|
260
|
+
WHERE scope=? AND id=? AND archived=0
|
|
261
|
+
`).run(
|
|
262
|
+
targetScope,
|
|
263
|
+
String(item.status || 'pending'),
|
|
264
|
+
item.type || null,
|
|
265
|
+
item.dispatched_to || item.agent || null,
|
|
266
|
+
item.parent_id || null,
|
|
267
|
+
_toMs(item.created_at || item.created),
|
|
268
|
+
_toMs(item.completed_at || item.completedAt),
|
|
269
|
+
JSON.stringify(item),
|
|
270
|
+
Date.now(),
|
|
271
|
+
sourceScope,
|
|
272
|
+
String(id),
|
|
273
|
+
);
|
|
274
|
+
return Number(result.changes) > 0;
|
|
275
|
+
});
|
|
276
|
+
if (moved) {
|
|
277
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
278
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
279
|
+
}
|
|
280
|
+
return moved;
|
|
281
|
+
}
|
|
282
|
+
|
|
188
283
|
module.exports = {
|
|
189
|
-
scopeForFilePath,
|
|
190
284
|
readWorkItemsForScope,
|
|
191
285
|
readAllWorkItems,
|
|
286
|
+
readArchivedWorkItemsForScope,
|
|
287
|
+
archiveWorkItem,
|
|
288
|
+
replaceArchivedWorkItems,
|
|
289
|
+
moveWorkItem,
|
|
192
290
|
applyWorkItemsMutation,
|
|
193
291
|
dropScope,
|
|
194
292
|
};
|
package/engine/worktree-gc.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// anchored by live keep_processes PIDs / managed_spawn cwds.
|
|
11
11
|
//
|
|
12
12
|
// 2. On engine boot (engine/cli.js): scan each project's worktree root
|
|
13
|
-
// for orphan W-* dirs whose dispatch is not
|
|
13
|
+
// for orphan W-* dirs whose dispatch is not active
|
|
14
14
|
// or pending) AND that are not pool entries. Such dirs are leftovers
|
|
15
15
|
// from a prior crash that block retries with
|
|
16
16
|
// `fatal: 'work/W-…' is already used by worktree at …`.
|
|
@@ -698,7 +698,7 @@ function gcDispatchWorktreeIfOrphan(opts) {
|
|
|
698
698
|
* Walk each project's worktree root and evict orphan W-* dirs.
|
|
699
699
|
*
|
|
700
700
|
* - `projects` — array of `{ name, localPath }`
|
|
701
|
-
* - `dispatchSnap` — `{ active: [...], pending: [...] }`
|
|
701
|
+
* - `dispatchSnap` — `{ active: [...], pending: [...] }` dispatch state
|
|
702
702
|
* - `worktreeRootRel` — relative dir under each project's localPath (default '../worktrees')
|
|
703
703
|
* - `log` — log(level, msg) function (optional)
|
|
704
704
|
* - `fs`/`removeWorktree`/`buildWorktreeDirName`/`listManagedSpecs` — test injection points
|
package/engine/worktree-pool.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* detached at `origin/<mainBranch>` and reused by the next dispatch for the
|
|
8
8
|
* same project — saving the cold install/build cost on subsequent WIs.
|
|
9
9
|
*
|
|
10
|
-
* Concurrency: all state flips
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* then
|
|
10
|
+
* Concurrency: all state flips use a SQLite transaction so two spawning ticks
|
|
11
|
+
* cannot double-borrow the same idle entry. Git commands never run inside the
|
|
12
|
+
* transaction — callers must do `tryBorrow`, then run git in their own scope,
|
|
13
|
+
* then call `evictEntry` on failure.
|
|
14
14
|
*
|
|
15
15
|
* Idle worktrees are detached at origin/<main>, NOT checked out on the local
|
|
16
16
|
* `main` branch. Local-main is typically already checked out in the project
|
|
@@ -22,16 +22,12 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const shared = require('./shared');
|
|
25
|
+
const smallStateStore = require('./small-state-store');
|
|
25
26
|
|
|
26
|
-
const { log, ENGINE_DEFAULTS
|
|
27
|
+
const { log, ENGINE_DEFAULTS } = shared;
|
|
27
28
|
|
|
28
|
-
const WORKTREE_POOL_FILENAME = 'worktree-pool.json';
|
|
29
29
|
const POOL_STATE = Object.freeze({ IDLE: 'idle', BORROWED: 'borrowed' });
|
|
30
30
|
|
|
31
|
-
function getPoolPath() {
|
|
32
|
-
return path.join(shared.MINIONS_DIR, 'engine', WORKTREE_POOL_FILENAME);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
function _normalizePath(p) {
|
|
36
32
|
if (!p) return '';
|
|
37
33
|
let resolved;
|
|
@@ -55,17 +51,13 @@ function _entriesArray(data) {
|
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
function readPool() {
|
|
58
|
-
const data =
|
|
54
|
+
const data = smallStateStore.readWorktreePool();
|
|
59
55
|
if (!data || !Array.isArray(data.entries)) return { entries: [] };
|
|
60
56
|
return data;
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
function mutateWorktreePool(mutator) {
|
|
64
|
-
|
|
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) => {
|
|
60
|
+
return smallStateStore.applyWorktreePoolMutation((data) => {
|
|
69
61
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = { entries: [] };
|
|
70
62
|
if (!Array.isArray(data.entries)) data.entries = [];
|
|
71
63
|
const next = mutator(data);
|
|
@@ -320,9 +312,7 @@ function getIdlePaths() {
|
|
|
320
312
|
}
|
|
321
313
|
|
|
322
314
|
module.exports = {
|
|
323
|
-
WORKTREE_POOL_FILENAME,
|
|
324
315
|
POOL_STATE,
|
|
325
|
-
getPoolPath,
|
|
326
316
|
readPool,
|
|
327
317
|
mutateWorktreePool,
|
|
328
318
|
getProjectPoolSize,
|