@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
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
const transport = require('./acp-transport');
|
|
62
|
-
const { Worker, hashMcpServers } = transport;
|
|
62
|
+
const { Worker, hashMcpServers, hashProcessContext } = transport;
|
|
63
63
|
|
|
64
64
|
// Test seam — mirrors engine/cc-worker-pool.js's `_internals` pattern, but is
|
|
65
65
|
// its OWN object (constructor-injected into Worker per acquisition) so this
|
|
@@ -106,11 +106,13 @@ let _nextWorkerId = 1;
|
|
|
106
106
|
const _free = []; // Worker[] — idle, unleased
|
|
107
107
|
const _busy = new Map(); // dispatchId → Worker — currently leased
|
|
108
108
|
const _queue = []; // pending acquire requests, FIFO
|
|
109
|
+
const _leaseContexts = new Map(); // dispatchId → mutable session-only context
|
|
109
110
|
// Slots reserved for a worker that's mid-reuse/respawn/fresh-spawn — pulled
|
|
110
111
|
// out of `_free` (or not yet spawned) but not yet placed in `_busy`. Counted
|
|
111
112
|
// toward `_poolSize` so a burst of concurrent acquireWorker calls can't
|
|
112
113
|
// overshoot the configured pool size while spawns are still in flight.
|
|
113
114
|
let _reserved = 0;
|
|
115
|
+
let _draining = false;
|
|
114
116
|
|
|
115
117
|
function _occupied() {
|
|
116
118
|
return _free.length + _busy.size + _reserved;
|
|
@@ -130,7 +132,10 @@ function _pump() {
|
|
|
130
132
|
while (_queue.length > 0) {
|
|
131
133
|
const req = _queue[0];
|
|
132
134
|
|
|
133
|
-
const matchIdx = _free.findIndex((w) =>
|
|
135
|
+
const matchIdx = _free.findIndex((w) =>
|
|
136
|
+
w.mcpServersHash === req.mcpServersHash
|
|
137
|
+
&& w.processContextHash === req.processContextHash
|
|
138
|
+
);
|
|
134
139
|
if (matchIdx >= 0) {
|
|
135
140
|
_queue.shift();
|
|
136
141
|
const worker = _free.splice(matchIdx, 1)[0];
|
|
@@ -165,6 +170,11 @@ async function _reuseWorker(worker, req) {
|
|
|
165
170
|
await worker.newSession({ mcpServers: req.mcpServers, model: req.model, effort: req.effort, cwd: req.cwd });
|
|
166
171
|
_trace(`dispatch=${req.dispatchId} reuse: worker=${worker.id} sessionId=${worker.sessionId}`);
|
|
167
172
|
_reserved--;
|
|
173
|
+
if (_draining) {
|
|
174
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
175
|
+
req.reject(new Error('agent-worker-pool: draining'));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
168
178
|
req.resolve(worker);
|
|
169
179
|
} catch (err) {
|
|
170
180
|
_reserved--;
|
|
@@ -181,6 +191,11 @@ async function _respawnWorker(stale, req) {
|
|
|
181
191
|
try {
|
|
182
192
|
const worker = await _bootWorker(req);
|
|
183
193
|
_reserved--;
|
|
194
|
+
if (_draining) {
|
|
195
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
196
|
+
req.reject(new Error('agent-worker-pool: draining'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
184
199
|
req.resolve(worker);
|
|
185
200
|
} catch (err) {
|
|
186
201
|
_reserved--;
|
|
@@ -193,6 +208,11 @@ async function _spawnFreshWorker(req) {
|
|
|
193
208
|
try {
|
|
194
209
|
const worker = await _bootWorker(req);
|
|
195
210
|
_reserved--;
|
|
211
|
+
if (_draining) {
|
|
212
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
213
|
+
req.reject(new Error('agent-worker-pool: draining'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
196
216
|
req.resolve(worker);
|
|
197
217
|
} catch (err) {
|
|
198
218
|
_reserved--;
|
|
@@ -215,6 +235,8 @@ async function _bootWorker(req) {
|
|
|
215
235
|
effort: req.effort,
|
|
216
236
|
mcpServers: req.mcpServers,
|
|
217
237
|
mcpServersHash: req.mcpServersHash,
|
|
238
|
+
processEnv: req.processEnv,
|
|
239
|
+
processContextHash: req.processContextHash,
|
|
218
240
|
cwd: req.cwd,
|
|
219
241
|
internals: _internals,
|
|
220
242
|
trace: _trace,
|
|
@@ -246,6 +268,7 @@ function _onWorkerExit(worker) {
|
|
|
246
268
|
for (const [dispatchId, w] of _busy) {
|
|
247
269
|
if (w === worker) {
|
|
248
270
|
_busy.delete(dispatchId);
|
|
271
|
+
_clearLeaseContext(dispatchId);
|
|
249
272
|
removed = true;
|
|
250
273
|
break;
|
|
251
274
|
}
|
|
@@ -272,6 +295,7 @@ async function _applyHermeticDirs(worker, hermeticDirs) {
|
|
|
272
295
|
worker.stream(`/add-dir ${dir}`, {
|
|
273
296
|
onDone: () => { _trace(`worker=${worker.id} add-dir ok: ${dir}`); resolve(); },
|
|
274
297
|
onError: (err) => { _trace(`worker=${worker.id} add-dir failed: ${dir} (${err && err.message})`); resolve(); },
|
|
298
|
+
deferSystemPrompt: true,
|
|
275
299
|
}).catch(() => resolve());
|
|
276
300
|
});
|
|
277
301
|
}
|
|
@@ -279,9 +303,20 @@ async function _applyHermeticDirs(worker, hermeticDirs) {
|
|
|
279
303
|
|
|
280
304
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
281
305
|
|
|
282
|
-
|
|
306
|
+
function _clearLeaseContext(dispatchId) {
|
|
307
|
+
const context = _leaseContexts.get(dispatchId);
|
|
308
|
+
if (!context) return false;
|
|
309
|
+
for (const key of Object.keys(context)) delete context[key];
|
|
310
|
+
_leaseContexts.delete(dispatchId);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function acquireWorker({
|
|
315
|
+
dispatchId, cwd, model, effort, mcpServers, hermeticDirs, processEnv, sessionContext,
|
|
316
|
+
} = {}) {
|
|
283
317
|
if (!dispatchId) throw new Error('agent-worker-pool.acquireWorker: dispatchId is required');
|
|
284
|
-
if (
|
|
318
|
+
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
319
|
+
if (_busy.has(dispatchId) || _leaseContexts.has(dispatchId)) {
|
|
285
320
|
throw new Error(
|
|
286
321
|
`agent-worker-pool.acquireWorker: dispatchId ${dispatchId} already has an active worker ` +
|
|
287
322
|
'(1 worker : 1 active dispatch invariant — call release() before acquiring again)'
|
|
@@ -289,17 +324,42 @@ async function acquireWorker({ dispatchId, cwd, model, effort, mcpServers, herme
|
|
|
289
324
|
}
|
|
290
325
|
|
|
291
326
|
const mcpServersHash = hashMcpServers(mcpServers);
|
|
292
|
-
const
|
|
327
|
+
const workerProcessEnv = processEnv && typeof processEnv === 'object'
|
|
328
|
+
? Object.freeze({ ...processEnv })
|
|
329
|
+
: undefined;
|
|
330
|
+
const processContextHash = hashProcessContext(workerProcessEnv);
|
|
331
|
+
const leaseContext = sessionContext && typeof sessionContext === 'object'
|
|
332
|
+
? { ...sessionContext }
|
|
333
|
+
: {};
|
|
334
|
+
const req = {
|
|
335
|
+
dispatchId, cwd, model, effort, mcpServers, mcpServersHash,
|
|
336
|
+
processEnv: workerProcessEnv, processContextHash,
|
|
337
|
+
};
|
|
338
|
+
_leaseContexts.set(dispatchId, leaseContext);
|
|
293
339
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
340
|
+
let worker;
|
|
341
|
+
try {
|
|
342
|
+
worker = await new Promise((resolve, reject) => {
|
|
343
|
+
req.resolve = resolve;
|
|
344
|
+
req.reject = reject;
|
|
345
|
+
_queue.push(req);
|
|
346
|
+
_pump();
|
|
347
|
+
});
|
|
300
348
|
|
|
301
|
-
|
|
302
|
-
|
|
349
|
+
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
350
|
+
_busy.set(dispatchId, worker);
|
|
351
|
+
await _applyHermeticDirs(worker, hermeticDirs);
|
|
352
|
+
if (worker.killed) throw new Error('agent-worker-pool: worker died during acquisition');
|
|
353
|
+
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (_busy.get(dispatchId) === worker) _busy.delete(dispatchId);
|
|
356
|
+
_clearLeaseContext(dispatchId);
|
|
357
|
+
if (worker) {
|
|
358
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
359
|
+
_pump();
|
|
360
|
+
}
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
303
363
|
|
|
304
364
|
return {
|
|
305
365
|
dispatchId,
|
|
@@ -311,8 +371,20 @@ async function acquireWorker({ dispatchId, cwd, model, effort, mcpServers, herme
|
|
|
311
371
|
// captured copy), since sessionId can rotate under a reused worker.
|
|
312
372
|
get pid() { return (worker.proc && worker.proc.pid) || null; },
|
|
313
373
|
get sessionId() { return worker.sessionId || null; },
|
|
314
|
-
stream: (promptText, opts) =>
|
|
315
|
-
|
|
374
|
+
stream: async (promptText, opts = {}) => {
|
|
375
|
+
try {
|
|
376
|
+
return await worker.stream(promptText, {
|
|
377
|
+
...opts,
|
|
378
|
+
trustedSessionContext: leaseContext,
|
|
379
|
+
});
|
|
380
|
+
} finally {
|
|
381
|
+
_clearLeaseContext(dispatchId);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
cancel: () => {
|
|
385
|
+
_clearLeaseContext(dispatchId);
|
|
386
|
+
return worker.cancel();
|
|
387
|
+
},
|
|
316
388
|
release: () => release(dispatchId),
|
|
317
389
|
};
|
|
318
390
|
}
|
|
@@ -325,6 +397,7 @@ async function acquireWorker({ dispatchId, cwd, model, effort, mcpServers, herme
|
|
|
325
397
|
// never acquired) — safe to call defensively.
|
|
326
398
|
function release(dispatchId) {
|
|
327
399
|
const worker = _busy.get(dispatchId);
|
|
400
|
+
_clearLeaseContext(dispatchId);
|
|
328
401
|
if (!worker) return false;
|
|
329
402
|
_busy.delete(dispatchId);
|
|
330
403
|
if (worker.killed) {
|
|
@@ -332,14 +405,44 @@ function release(dispatchId) {
|
|
|
332
405
|
_pump();
|
|
333
406
|
return true;
|
|
334
407
|
}
|
|
408
|
+
if (_draining) {
|
|
409
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
410
|
+
_trace(`dispatch=${dispatchId} release: draining, worker=${worker.id} closed`);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
335
413
|
_free.push(worker);
|
|
336
414
|
_trace(`dispatch=${dispatchId} release: worker=${worker.id} returned to free set`);
|
|
337
415
|
_pump();
|
|
338
416
|
return true;
|
|
339
417
|
}
|
|
340
418
|
|
|
419
|
+
function beginDrain() {
|
|
420
|
+
if (_draining) return;
|
|
421
|
+
_draining = true;
|
|
422
|
+
for (const req of _queue.splice(0)) {
|
|
423
|
+
try { req.reject(new Error('agent-worker-pool: draining')); } catch { /* already settled */ }
|
|
424
|
+
}
|
|
425
|
+
for (const worker of _free.splice(0)) {
|
|
426
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isDraining() {
|
|
431
|
+
return _draining;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function isDrained() {
|
|
435
|
+
return _busy.size === 0 && _reserved === 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getActiveLeaseIds() {
|
|
439
|
+
return [..._busy.keys()];
|
|
440
|
+
}
|
|
441
|
+
|
|
341
442
|
function shutdown() {
|
|
443
|
+
_draining = true;
|
|
342
444
|
for (const req of _queue.splice(0)) {
|
|
445
|
+
_clearLeaseContext(req.dispatchId);
|
|
343
446
|
try { req.reject(new Error('agent-worker-pool: shutdown')); } catch { /* swallow */ }
|
|
344
447
|
}
|
|
345
448
|
for (const worker of _free.splice(0)) {
|
|
@@ -349,6 +452,7 @@ function shutdown() {
|
|
|
349
452
|
try { worker.close(); } catch { /* swallow */ }
|
|
350
453
|
}
|
|
351
454
|
_busy.clear();
|
|
455
|
+
for (const dispatchId of [..._leaseContexts.keys()]) _clearLeaseContext(dispatchId);
|
|
352
456
|
_reserved = 0;
|
|
353
457
|
}
|
|
354
458
|
|
|
@@ -357,6 +461,10 @@ module.exports = {
|
|
|
357
461
|
release,
|
|
358
462
|
setPoolSize,
|
|
359
463
|
getPoolSize,
|
|
464
|
+
beginDrain,
|
|
465
|
+
isDraining,
|
|
466
|
+
isDrained,
|
|
467
|
+
getActiveLeaseIds,
|
|
360
468
|
shutdown,
|
|
361
469
|
DEFAULT_POOL_SIZE,
|
|
362
470
|
// Exposed for unit tests; engine code MUST go through the public API.
|
|
@@ -364,4 +472,5 @@ module.exports = {
|
|
|
364
472
|
_free,
|
|
365
473
|
_busy,
|
|
366
474
|
_queue,
|
|
475
|
+
_leaseContexts,
|
|
367
476
|
};
|
package/engine/bridge.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/bridge.js — Constellation-bridge config + marker accessors.
|
|
3
3
|
*
|
|
4
|
-
* Pure helpers for the `minions bridge ...` subcommand and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* marker-file contract, and atomic config writes for the toggle.
|
|
4
|
+
* Pure helpers for the `minions bridge ...` subcommand and the versioned
|
|
5
|
+
* metadata snapshot consumed by Constellation. Minions remains authoritative:
|
|
6
|
+
* the Constellation agent polls the local dashboard and mirrors only the
|
|
7
|
+
* allowlisted projection returned by buildBridgeSnapshot().
|
|
9
8
|
*
|
|
10
9
|
* Strict semantics: only the literal boolean `true` enables the bridge.
|
|
11
10
|
* Mirror this check on the Constellation reader to avoid truthy coercion
|
|
@@ -24,6 +23,258 @@ const {
|
|
|
24
23
|
} = shared;
|
|
25
24
|
|
|
26
25
|
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
26
|
+
const BRIDGE_SNAPSHOT_PROTOCOL = 'minions.constellation.snapshot';
|
|
27
|
+
const BRIDGE_SNAPSHOT_PROTOCOL_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
const SNAPSHOT_CAPS = Object.freeze({
|
|
30
|
+
id: 128,
|
|
31
|
+
status: 128,
|
|
32
|
+
agent: 128,
|
|
33
|
+
title: 512,
|
|
34
|
+
project: 128,
|
|
35
|
+
url: 2048,
|
|
36
|
+
timestamp: 64,
|
|
37
|
+
shortLabel: 128,
|
|
38
|
+
branch: 255,
|
|
39
|
+
name: 128,
|
|
40
|
+
});
|
|
41
|
+
const INACTIVE_AGENT_STATUSES = new Set(['idle', 'available', 'sleeping']);
|
|
42
|
+
|
|
43
|
+
function _string(value, max, fallback = null) {
|
|
44
|
+
if (typeof value !== 'string') return fallback;
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
if (!trimmed) return fallback;
|
|
47
|
+
return trimmed.slice(0, max);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _isoTime(value) {
|
|
51
|
+
if (value === null || value === undefined || value === '') return undefined;
|
|
52
|
+
const ms = typeof value === 'number' ? value : Date.parse(String(value));
|
|
53
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
54
|
+
return new Date(ms).toISOString().slice(0, SNAPSHOT_CAPS.timestamp);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _defined(obj) {
|
|
58
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _projectList(items, projector) {
|
|
62
|
+
if (!Array.isArray(items)) return [];
|
|
63
|
+
const projected = [];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (!item || typeof item !== 'object') continue;
|
|
66
|
+
const value = projector(item);
|
|
67
|
+
if (value) projected.push(value);
|
|
68
|
+
}
|
|
69
|
+
return projected;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _projectWorkItem(item) {
|
|
73
|
+
const id = _string(item.id, SNAPSHOT_CAPS.id);
|
|
74
|
+
if (!id) return null;
|
|
75
|
+
return _defined({
|
|
76
|
+
id,
|
|
77
|
+
title: _string(item.title, SNAPSHOT_CAPS.title, id),
|
|
78
|
+
type: _string(item.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
79
|
+
priority: _string(item.priority, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
80
|
+
status: _string(item.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
81
|
+
project: _string(item.project, SNAPSHOT_CAPS.project) || undefined,
|
|
82
|
+
agent: _string(item.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
83
|
+
createdBy: _string(item.createdBy, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
84
|
+
created: _isoTime(item.created),
|
|
85
|
+
dispatched_to: _string(item.dispatched_to, SNAPSHOT_CAPS.agent) || undefined,
|
|
86
|
+
dispatched_at: _isoTime(item.dispatched_at),
|
|
87
|
+
completedAt: _isoTime(item.completedAt || item.completed_at),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _projectPullRequest(pr) {
|
|
92
|
+
const id = _string(pr.id, SNAPSHOT_CAPS.id);
|
|
93
|
+
const prNumber = Number(pr.prNumber);
|
|
94
|
+
const url = _string(pr.url, SNAPSHOT_CAPS.url);
|
|
95
|
+
if (!id || !url || !Number.isInteger(prNumber) || prNumber < 0) return null;
|
|
96
|
+
return _defined({
|
|
97
|
+
id,
|
|
98
|
+
prNumber,
|
|
99
|
+
title: _string(pr.title, SNAPSHOT_CAPS.title, id),
|
|
100
|
+
agent: _string(pr.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
101
|
+
branch: _string(pr.branch, SNAPSHOT_CAPS.branch) || undefined,
|
|
102
|
+
reviewStatus: _string(pr.reviewStatus, SNAPSHOT_CAPS.status, 'unknown'),
|
|
103
|
+
status: _string(pr.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
104
|
+
url,
|
|
105
|
+
created: _isoTime(pr.created),
|
|
106
|
+
lastReviewedAt: _isoTime(pr.lastReviewedAt),
|
|
107
|
+
lastPushedAt: _isoTime(pr.lastPushedAt),
|
|
108
|
+
lastDispatchedAt: _isoTime(pr.lastDispatchedAt),
|
|
109
|
+
mergeStatus: _string(pr.mergeStatus || pr.mergeStateStatus, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
110
|
+
mergeable: typeof pr.mergeable === 'boolean' ? pr.mergeable : undefined,
|
|
111
|
+
isDraft: typeof pr.isDraft === 'boolean' ? pr.isDraft : undefined,
|
|
112
|
+
behindBy: Number.isInteger(pr.behindBy) && pr.behindBy >= 0 ? pr.behindBy : undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _projectPlan(plan) {
|
|
117
|
+
const id = _string(plan.id || plan.file, SNAPSHOT_CAPS.id);
|
|
118
|
+
if (!id) return null;
|
|
119
|
+
return _defined({
|
|
120
|
+
id,
|
|
121
|
+
title: _string(plan.title || plan.summary || plan.plan_summary, SNAPSHOT_CAPS.title, id),
|
|
122
|
+
status: _string(plan.status, SNAPSHOT_CAPS.status, plan.archived ? 'archived' : 'unknown'),
|
|
123
|
+
agent: _string(plan.agent || plan.generatedBy || plan.generated_by, SNAPSHOT_CAPS.agent) || undefined,
|
|
124
|
+
project: _string(plan.project, SNAPSHOT_CAPS.project) || undefined,
|
|
125
|
+
createdAt: _isoTime(plan.createdAt || plan.generatedAt || plan.generated_at),
|
|
126
|
+
updatedAt: _isoTime(plan.updatedAt),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _projectSchedule(schedule) {
|
|
131
|
+
const id = _string(schedule.id, SNAPSHOT_CAPS.id);
|
|
132
|
+
if (!id) return null;
|
|
133
|
+
return _defined({
|
|
134
|
+
id,
|
|
135
|
+
title: _string(schedule.title, SNAPSHOT_CAPS.title, id),
|
|
136
|
+
status: _string(schedule.status, SNAPSHOT_CAPS.status,
|
|
137
|
+
schedule.enabled === false ? 'disabled' : 'enabled'),
|
|
138
|
+
type: _string(schedule.type, SNAPSHOT_CAPS.shortLabel, 'schedule'),
|
|
139
|
+
project: _string(schedule.project, SNAPSHOT_CAPS.project) || undefined,
|
|
140
|
+
agent: _string(schedule.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
141
|
+
nextRun: _isoTime(schedule.nextRun),
|
|
142
|
+
lastRun: _isoTime(schedule.lastRun || schedule._lastRun || schedule._lastCompletedAt),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _projectDispatch(entry, status) {
|
|
147
|
+
const id = _string(entry.id, SNAPSHOT_CAPS.id);
|
|
148
|
+
if (!id) return null;
|
|
149
|
+
const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
|
|
150
|
+
const metaItem = meta.item && typeof meta.item === 'object' ? meta.item : {};
|
|
151
|
+
const metaProject = meta.project && typeof meta.project === 'object' ? meta.project : {};
|
|
152
|
+
return _defined({
|
|
153
|
+
id,
|
|
154
|
+
agent: _string(entry.agent, SNAPSHOT_CAPS.agent, 'unassigned'),
|
|
155
|
+
status: _string(entry.status, SNAPSHOT_CAPS.status, status),
|
|
156
|
+
type: _string(entry.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
157
|
+
project: _string(entry.project || metaProject.name, SNAPSHOT_CAPS.project) || undefined,
|
|
158
|
+
workItemId: _string(entry.workItemId || metaItem.id, SNAPSHOT_CAPS.id) || undefined,
|
|
159
|
+
prId: _string(entry.prId || entry.pr_id, SNAPSHOT_CAPS.id) || undefined,
|
|
160
|
+
startedAt: _isoTime(entry.startedAt || entry.started_at),
|
|
161
|
+
completedAt: _isoTime(entry.completedAt || entry.completed_at),
|
|
162
|
+
outcome: _string(entry.outcome || entry.result, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _projectDispatchQueue(dispatch) {
|
|
167
|
+
if (!dispatch || typeof dispatch !== 'object') return [];
|
|
168
|
+
const projected = [];
|
|
169
|
+
for (const [bucket, status] of [
|
|
170
|
+
['pending', 'queued'],
|
|
171
|
+
['active', 'active'],
|
|
172
|
+
['review', 'review'],
|
|
173
|
+
['completed', 'completed'],
|
|
174
|
+
]) {
|
|
175
|
+
const entries = bucket === 'completed' && Array.isArray(dispatch[bucket])
|
|
176
|
+
? dispatch[bucket].slice(-20)
|
|
177
|
+
: dispatch[bucket];
|
|
178
|
+
projected.push(..._projectList(entries, entry => _projectDispatch(entry, status)));
|
|
179
|
+
}
|
|
180
|
+
return projected;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function _projectAgent(agent) {
|
|
184
|
+
const id = _string(agent.id || agent.agent, SNAPSHOT_CAPS.agent);
|
|
185
|
+
if (!id) return null;
|
|
186
|
+
return _defined({
|
|
187
|
+
agent: id,
|
|
188
|
+
status: _string(agent.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
189
|
+
project: _string(agent.project, SNAPSHOT_CAPS.project) || undefined,
|
|
190
|
+
currentTaskId: _string(agent.currentTaskId || agent.currentDispatchId, SNAPSHOT_CAPS.id) || undefined,
|
|
191
|
+
currentTaskTitle: _string(agent.currentTaskTitle || agent.currentTask, SNAPSHOT_CAPS.title) || undefined,
|
|
192
|
+
updatedAt: _isoTime(agent.updatedAt || agent.lastActionAt),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the complete metadata-only state projection consumed by Constellation.
|
|
198
|
+
* Every field is copied explicitly so free-text task descriptions, prompts,
|
|
199
|
+
* completion reports, diffs, comments, paths, logs, and credentials cannot
|
|
200
|
+
* cross the bridge accidentally.
|
|
201
|
+
*/
|
|
202
|
+
function buildBridgeSnapshot(state = {}) {
|
|
203
|
+
const generatedAt = _isoTime(state.generatedAt) || new Date().toISOString();
|
|
204
|
+
const engine = state.control && typeof state.control === 'object' ? state.control : {};
|
|
205
|
+
const workItems = _projectList(state.workItems, _projectWorkItem);
|
|
206
|
+
const pullRequests = _projectList(state.pullRequests, _projectPullRequest);
|
|
207
|
+
const plans = _projectList(state.plans, _projectPlan);
|
|
208
|
+
const schedules = _projectList(state.schedules, _projectSchedule);
|
|
209
|
+
const agents = _projectList(state.agents, _projectAgent);
|
|
210
|
+
const dispatches = _projectDispatchQueue(state.dispatch);
|
|
211
|
+
const activeAgents = agents.filter(agent => !INACTIVE_AGENT_STATUSES.has(agent.status.toLowerCase())).length;
|
|
212
|
+
const minionsVersion = _string(state.minionsVersion, SNAPSHOT_CAPS.shortLabel);
|
|
213
|
+
const engineState = _string(engine.state, SNAPSHOT_CAPS.status, 'unknown');
|
|
214
|
+
const enginePid = Number.isInteger(engine.pid) && engine.pid > 0 ? engine.pid : null;
|
|
215
|
+
const projects = Array.isArray(state.projects) ? state.projects : [];
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
protocol: BRIDGE_SNAPSHOT_PROTOCOL,
|
|
219
|
+
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
220
|
+
complete: true,
|
|
221
|
+
generatedAt,
|
|
222
|
+
minionsVersion,
|
|
223
|
+
engine: {
|
|
224
|
+
state: engineState,
|
|
225
|
+
pid: enginePid,
|
|
226
|
+
},
|
|
227
|
+
entities: {
|
|
228
|
+
work_item: workItems,
|
|
229
|
+
pr: pullRequests,
|
|
230
|
+
plan: plans,
|
|
231
|
+
schedule: schedules,
|
|
232
|
+
dispatch: dispatches,
|
|
233
|
+
agent_status: agents,
|
|
234
|
+
device_summary: [{
|
|
235
|
+
name: 'Minions',
|
|
236
|
+
status: engineState,
|
|
237
|
+
lastSeen: generatedAt,
|
|
238
|
+
agentCount: agents.length,
|
|
239
|
+
activeAgentCount: activeAgents,
|
|
240
|
+
workItemCount: workItems.length,
|
|
241
|
+
pullRequestCount: pullRequests.length,
|
|
242
|
+
planCount: plans.length,
|
|
243
|
+
scheduleCount: schedules.length,
|
|
244
|
+
dispatchCount: dispatches.length,
|
|
245
|
+
projectCount: projects.length,
|
|
246
|
+
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
247
|
+
minionsVersion,
|
|
248
|
+
snapshotGeneratedAt: generatedAt,
|
|
249
|
+
enginePid,
|
|
250
|
+
}],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function projectSnapshotHealthForBridge(snapshot) {
|
|
256
|
+
if (!snapshot || typeof snapshot !== 'object') return null;
|
|
257
|
+
if (snapshot.protocol !== BRIDGE_SNAPSHOT_PROTOCOL) return null;
|
|
258
|
+
if (snapshot.protocolVersion !== BRIDGE_SNAPSHOT_PROTOCOL_VERSION) return null;
|
|
259
|
+
if (snapshot.complete !== true || !snapshot.entities || typeof snapshot.entities !== 'object') return null;
|
|
260
|
+
const entities = snapshot.entities;
|
|
261
|
+
const summary = Array.isArray(entities.device_summary) ? entities.device_summary[0] || {} : {};
|
|
262
|
+
const count = key => (Array.isArray(entities[key]) ? entities[key].length : 0);
|
|
263
|
+
return {
|
|
264
|
+
protocolVersion: snapshot.protocolVersion,
|
|
265
|
+
minionsVersion: snapshot.minionsVersion ?? null,
|
|
266
|
+
engineState: snapshot.engine?.state ?? null,
|
|
267
|
+
enginePid: snapshot.engine?.pid ?? null,
|
|
268
|
+
agentCount: count('agent_status'),
|
|
269
|
+
activeAgentCount: summary.activeAgentCount ?? null,
|
|
270
|
+
workItemCount: count('work_item'),
|
|
271
|
+
pullRequestCount: count('pr'),
|
|
272
|
+
planCount: count('plan'),
|
|
273
|
+
scheduleCount: count('schedule'),
|
|
274
|
+
dispatchCount: count('dispatch'),
|
|
275
|
+
projectCount: summary.projectCount ?? null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
27
278
|
|
|
28
279
|
/**
|
|
29
280
|
* Strict check: `true` ⇔ bridge enabled. Any other shape (missing field,
|
|
@@ -118,7 +369,11 @@ module.exports = {
|
|
|
118
369
|
readBridgeMarker,
|
|
119
370
|
setBridgeEnabled,
|
|
120
371
|
formatRelativeAge,
|
|
372
|
+
buildBridgeSnapshot,
|
|
373
|
+
projectSnapshotHealthForBridge,
|
|
121
374
|
projectStatusForBridge,
|
|
375
|
+
BRIDGE_SNAPSHOT_PROTOCOL,
|
|
376
|
+
BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
122
377
|
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
123
378
|
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
124
379
|
};
|