@yemi33/minions 0.1.2362 → 0.1.2364
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/dashboard/js/command-center.js +3 -0
- package/dashboard.js +46 -18
- package/docs/architecture-review-2026-07-09.md +94 -0
- package/docs/command-center.md +2 -0
- package/engine/lifecycle.js +41 -1
- package/engine/playbook.js +5 -5
- package/engine/process-utils.js +18 -7
- package/engine/queries.js +22 -5
- package/engine/shared.js +28 -1
- package/engine/worktree-gc.js +2 -2
- package/engine.js +5 -2
- package/package.json +1 -1
|
@@ -1286,6 +1286,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1286
1286
|
updateStreamDiv();
|
|
1287
1287
|
} else if (evt.type === 'heartbeat') {
|
|
1288
1288
|
return;
|
|
1289
|
+
} else if (evt.type === 'status') {
|
|
1290
|
+
streamStatusNote = evt.message || '';
|
|
1291
|
+
updateStreamDiv();
|
|
1289
1292
|
} else if (evt.type === 'tool') {
|
|
1290
1293
|
ccSegmentsApplyTool(segments, { name: evt.name, input: evt.input || {}, id: evt.id || null });
|
|
1291
1294
|
if (activeTab) activeTab._segments = segments;
|
package/dashboard.js
CHANGED
|
@@ -2644,15 +2644,14 @@ function _getMtimes() {
|
|
|
2644
2644
|
return result;
|
|
2645
2645
|
}
|
|
2646
2646
|
|
|
2647
|
-
// Reset
|
|
2648
|
-
//
|
|
2649
|
-
// (`_mcpServersCache` 5min) are the two with TTLs longer than our poll
|
|
2650
|
-
// cadence, and both back slices the user sees on screen. Called on every
|
|
2651
|
-
// mtime-detected rebuild so the rebuild always reads fresh disk state.
|
|
2647
|
+
// Reset per-source caches whose TTL would otherwise survive an mtime-triggered
|
|
2648
|
+
// status rebuild and serve stale data.
|
|
2652
2649
|
function _invalidateInnerCachesForRebuild() {
|
|
2653
2650
|
try { queries.invalidateSkillsCache(); } catch { /* optional */ }
|
|
2654
2651
|
_mcpServersCache = null;
|
|
2655
2652
|
_mcpServersCacheTs = 0;
|
|
2653
|
+
_diskVersionCache = null;
|
|
2654
|
+
_diskVersionCacheTs = 0;
|
|
2656
2655
|
}
|
|
2657
2656
|
|
|
2658
2657
|
function _mtimesChanged(prev, curr) {
|
|
@@ -3706,6 +3705,7 @@ function _ensureCcLiveStream(tabId) {
|
|
|
3706
3705
|
tabId,
|
|
3707
3706
|
text: '',
|
|
3708
3707
|
tools: [],
|
|
3708
|
+
statusMessage: '',
|
|
3709
3709
|
donePayload: null,
|
|
3710
3710
|
writer: null,
|
|
3711
3711
|
endResponse: null,
|
|
@@ -6341,6 +6341,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
6341
6341
|
if (!wiPath) {
|
|
6342
6342
|
const prdFile = body.prdFile;
|
|
6343
6343
|
if (!prdFile) return jsonReply(res, 404, { error: 'work item not found in any source' });
|
|
6344
|
+
// W-mrdykt9m0006e459 — prdFile is unauthenticated client input; reject
|
|
6345
|
+
// traversal/absolute paths and anything that resolves outside PRD_DIR
|
|
6346
|
+
// before it ever reaches a filesystem read.
|
|
6347
|
+
try { shared.sanitizePath(prdFile, PRD_DIR); }
|
|
6348
|
+
catch { return jsonReply(res, 400, { error: 'invalid prdFile path' }); }
|
|
6344
6349
|
|
|
6345
6350
|
// Look up PRD item to create a new work item on-demand.
|
|
6346
6351
|
// safeJsonNoRestore — PRDs are terminal artifacts; a stale .backup
|
|
@@ -10048,6 +10053,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10048
10053
|
if (!hasImages && shared.resolveCcUseWorkerPool(engineConfig)) {
|
|
10049
10054
|
return _invokeCcStreamViaPool({ prompt, liveState, toolUses, model, effort, engineConfig, systemPrompt, tabId });
|
|
10050
10055
|
}
|
|
10056
|
+
const setStatus = (message) => {
|
|
10057
|
+
liveState.statusMessage = message;
|
|
10058
|
+
if (liveState.writer) liveState.writer({ type: 'status', message });
|
|
10059
|
+
};
|
|
10060
|
+
if (hasImages) {
|
|
10061
|
+
_touchCcLiveStream(liveState);
|
|
10062
|
+
setStatus('Looking at the image...');
|
|
10063
|
+
}
|
|
10051
10064
|
const { callLLMStreaming } = require('./engine/llm');
|
|
10052
10065
|
return callLLMStreaming(prompt, systemPrompt, {
|
|
10053
10066
|
timeout: CC_CALL_TIMEOUT_MS, label: 'command-center', model, maxTurns,
|
|
@@ -10056,11 +10069,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10056
10069
|
engineConfig, images,
|
|
10057
10070
|
onChunk: (text, segmentId) => {
|
|
10058
10071
|
_touchCcLiveStream(liveState);
|
|
10072
|
+
if (liveState.statusMessage) {
|
|
10073
|
+
setStatus('');
|
|
10074
|
+
}
|
|
10059
10075
|
liveState.text = text;
|
|
10060
10076
|
if (liveState.writer) liveState.writer({ type: 'chunk', text, segmentId });
|
|
10061
10077
|
},
|
|
10062
10078
|
onToolUse: (name, input, id) => {
|
|
10063
10079
|
_touchCcLiveStream(liveState);
|
|
10080
|
+
if (liveState.statusMessage) {
|
|
10081
|
+
setStatus('');
|
|
10082
|
+
}
|
|
10064
10083
|
// Claude (direct) now threads a tool_use id; with an id the chip starts
|
|
10065
10084
|
// 'pending' and can be flipped to completed/failed by onToolUpdate.
|
|
10066
10085
|
const entry = { name, input: input || {}, id: id || null, status: id ? 'pending' : null };
|
|
@@ -10515,6 +10534,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10515
10534
|
writeCcEvent({ type: 'tool-update', id: tool.id, status: tool.status });
|
|
10516
10535
|
}
|
|
10517
10536
|
}
|
|
10537
|
+
if (live.statusMessage) writeCcEvent({ type: 'status', message: live.statusMessage });
|
|
10518
10538
|
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
10519
10539
|
if (live.donePayload) {
|
|
10520
10540
|
writeCcEvent(live.donePayload);
|
|
@@ -11276,6 +11296,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11276
11296
|
} catch { /* unknown/free-text runtime */ }
|
|
11277
11297
|
return modelStr;
|
|
11278
11298
|
}
|
|
11299
|
+
const _isClear = (v) => v === '' || v === null;
|
|
11300
|
+
let _registeredCliNames = null;
|
|
11301
|
+
const _validCli = (name) => {
|
|
11302
|
+
if (_registeredCliNames == null) {
|
|
11303
|
+
try { _registeredCliNames = _engineRuntimes.listRuntimes(); }
|
|
11304
|
+
catch { _registeredCliNames = []; }
|
|
11305
|
+
}
|
|
11306
|
+
return _registeredCliNames.length === 0 || _registeredCliNames.includes(String(name));
|
|
11307
|
+
};
|
|
11279
11308
|
if (body.engine) {
|
|
11280
11309
|
const e = body.engine;
|
|
11281
11310
|
const D = shared.ENGINE_DEFAULTS;
|
|
@@ -11385,15 +11414,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11385
11414
|
// chooses)" option submits '' and we must persist that as "unset".
|
|
11386
11415
|
// Validate `defaultCli` and `ccCli` against the runtime registry so a
|
|
11387
11416
|
// typo in the dashboard can't pin the fleet to a non-existent runtime.
|
|
11388
|
-
const _isClear = (v) => v === '' || v === null;
|
|
11389
|
-
let _registeredCliNames = null;
|
|
11390
|
-
const _validCli = (name) => {
|
|
11391
|
-
if (_registeredCliNames == null) {
|
|
11392
|
-
try { _registeredCliNames = require('./engine/runtimes').listRuntimes(); }
|
|
11393
|
-
catch { _registeredCliNames = []; }
|
|
11394
|
-
}
|
|
11395
|
-
return _registeredCliNames.length === 0 || _registeredCliNames.includes(String(name));
|
|
11396
|
-
};
|
|
11397
11417
|
if (e.defaultCli !== undefined) {
|
|
11398
11418
|
if (_isClear(e.defaultCli)) _deleteEngineConfig('defaultCli');
|
|
11399
11419
|
else if (_validCli(e.defaultCli)) _setEngineConfig('defaultCli', String(e.defaultCli));
|
|
@@ -11589,8 +11609,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11589
11609
|
// CLI values pin the agent to a specific runtime. `0` is a valid
|
|
11590
11610
|
// maxBudgetUsd (read-only / dry-run agents).
|
|
11591
11611
|
if (updates.cli !== undefined) {
|
|
11592
|
-
if (updates.cli
|
|
11593
|
-
else config.agents[id].cli = String(updates.cli);
|
|
11612
|
+
if (_isClear(updates.cli)) delete config.agents[id].cli;
|
|
11613
|
+
else if (_validCli(updates.cli)) config.agents[id].cli = String(updates.cli);
|
|
11614
|
+
else _clamped.push(`agents.${id}.cli: "${updates.cli}" not registered (kept previous value)`);
|
|
11594
11615
|
}
|
|
11595
11616
|
if (updates.model !== undefined) {
|
|
11596
11617
|
if (updates.model === '' || updates.model === null) delete config.agents[id].model;
|
|
@@ -14113,8 +14134,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14113
14134
|
path.join(MINIONS_DIR, 'pull-requests.json'),
|
|
14114
14135
|
];
|
|
14115
14136
|
let found = false;
|
|
14137
|
+
// Issue #803: tombstone EVERY scope that has a copy of this PR id, not
|
|
14138
|
+
// just the first one found. When enrollPrFromCanonicalId (issue #802)
|
|
14139
|
+
// has left a stray cross-scope duplicate (e.g. a legit per-project row
|
|
14140
|
+
// plus a stray central-scope copy), stopping at the first match leaves
|
|
14141
|
+
// the other scope's copy fully untouched — queries.js#getPullRequests
|
|
14142
|
+
// then de-dupes by picking that untouched duplicate as the "winner"
|
|
14143
|
+
// and silently resurrects the "deleted" PR on the next read.
|
|
14116
14144
|
for (const prPath of prPaths) {
|
|
14117
|
-
if (found) break;
|
|
14118
14145
|
// Issue #384: use a tombstone (userDeleted:true) instead of splice so
|
|
14119
14146
|
// the next poller cycle cannot re-add the PR. The entry stays in the
|
|
14120
14147
|
// file but is filtered from API responses and skipped by pollers.
|
|
@@ -14895,7 +14922,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14895
14922
|
// dashboard route serves dashboard/slim.html instead of the full SPA.
|
|
14896
14923
|
// Checked at request time so /api/features/toggle flips behavior with
|
|
14897
14924
|
// no restart; flag-off means zero behavior change for the catch-all.
|
|
14898
|
-
|
|
14925
|
+
const fullDashboardRequested = new URL(req.url, 'http://localhost').searchParams.get('fullDashboard') === '1';
|
|
14926
|
+
if (pathname === '/' && !fullDashboardRequested && features.isFeatureOn('slim-ux', CONFIG)) {
|
|
14899
14927
|
return serveSlimUx(req, res);
|
|
14900
14928
|
}
|
|
14901
14929
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Architecture Review — 2026-07-09
|
|
2
|
+
|
|
3
|
+
Scope: engine orchestration, SQL/JSON state boundaries, runtime/process recovery,
|
|
4
|
+
worktree lifecycle, pipelines/schedules, dashboard/API settings, and prompt
|
|
5
|
+
assembly. Findings were verified against `origin/main` at `b1b83dd2`.
|
|
6
|
+
|
|
7
|
+
## Fixed in this change
|
|
8
|
+
|
|
9
|
+
### High: Codex processes were rejected by orphan recovery
|
|
10
|
+
|
|
11
|
+
`isProcessCommandLineMatchingAgent` recognized only Claude and Copilot. After an
|
|
12
|
+
engine restart, a live Codex dispatch could therefore be classified as a
|
|
13
|
+
recycled PID and retried while the original process was still running.
|
|
14
|
+
|
|
15
|
+
The process gate now recognizes all bundled runtime CLIs, including native and
|
|
16
|
+
npm-installed Codex and Claude layouts, without accepting unrelated substring
|
|
17
|
+
matches.
|
|
18
|
+
|
|
19
|
+
### High: worktree GC lost the project name from real dispatch rows
|
|
20
|
+
|
|
21
|
+
Worktree GC assumed `dispatch.meta.project` was a string, while production
|
|
22
|
+
dispatches store a project descriptor object. In shared worktree-root layouts,
|
|
23
|
+
the resulting `default` hash could fail to protect another project's active
|
|
24
|
+
worktree from orphan cleanup.
|
|
25
|
+
|
|
26
|
+
GC now resolves both the current object shape and the legacy string shape.
|
|
27
|
+
|
|
28
|
+
### Medium: invalid per-agent runtimes bypassed settings validation
|
|
29
|
+
|
|
30
|
+
Fleet runtime fields were validated against the adapter registry, but
|
|
31
|
+
`agents.<id>.cli` was persisted verbatim. A typo could poison future dispatches
|
|
32
|
+
with a runtime-resolution failure. Per-agent overrides now share the fleet
|
|
33
|
+
validation path and preserve the previous value on invalid input.
|
|
34
|
+
|
|
35
|
+
### Medium: the no-TTL status cache omitted its own source files
|
|
36
|
+
|
|
37
|
+
`/api/status` includes config-, install-, and package-derived fields but its
|
|
38
|
+
mtime registry omitted `config.json`, `.install-id`, and `package.json`.
|
|
39
|
+
Out-of-process changes could remain stale indefinitely. These inputs now
|
|
40
|
+
participate in cache invalidation.
|
|
41
|
+
|
|
42
|
+
### Medium: Slim UX trapped users away from advanced settings
|
|
43
|
+
|
|
44
|
+
Slim linked to `/?fullDashboard=1`, but root takeover ignored the query string
|
|
45
|
+
and served Slim again. The root dispatcher now honors that explicit classic-UX
|
|
46
|
+
escape hatch.
|
|
47
|
+
|
|
48
|
+
### Low: prompt validation generated warnings for valid templates
|
|
49
|
+
|
|
50
|
+
The validator required the literal `## Your Task`, while established playbooks
|
|
51
|
+
also use `## Task Description` and `## Mission`. Those valid aliases now satisfy
|
|
52
|
+
the task-section contract, eliminating recurring false-positive warnings.
|
|
53
|
+
|
|
54
|
+
### Low: cancelled cron work blocked future schedule fires
|
|
55
|
+
|
|
56
|
+
The dashboard run-now path already treated cancelled work as terminal, but the
|
|
57
|
+
engine's cron enqueue dedup did not. Both paths now allow a new run after
|
|
58
|
+
cancellation.
|
|
59
|
+
|
|
60
|
+
## Verified follow-up findings
|
|
61
|
+
|
|
62
|
+
These require separate focused changes rather than expanding this PR:
|
|
63
|
+
|
|
64
|
+
1. **Dispatch SQL/JSON empty-state resurrection (high).** A process that has not
|
|
65
|
+
written SQL can still adopt stale `dispatch.json` when the SQL table is empty.
|
|
66
|
+
The durable fix is a mirror-generation/hash marker or removal of the legacy
|
|
67
|
+
fallback, not moving filesystem writes inside a SQLite transaction.
|
|
68
|
+
2. **Pipeline task terminal-state model (high).** Task stages use a boolean
|
|
69
|
+
completion check and promote failed work items to a completed pipeline stage;
|
|
70
|
+
cancelled/review-blocked work can wedge indefinitely. Replace the boolean
|
|
71
|
+
helper with an explicit terminal outcome (`completed`, `failed`,
|
|
72
|
+
`waiting-human`, non-terminal) and propagate it through parallel stages.
|
|
73
|
+
3. **Settings save spans two independent writes (medium).** Config and routing
|
|
74
|
+
are saved through separate endpoints, so routing failure can leave a partial
|
|
75
|
+
configuration update. A combined transactional settings contract should
|
|
76
|
+
replace the client-side two-step sequence.
|
|
77
|
+
4. **ADO identity lookup duplication (medium).** PR polling resolves the same
|
|
78
|
+
authenticated user more than once per PR. Cache it once per organization and
|
|
79
|
+
poll pass to reduce throttling pressure.
|
|
80
|
+
5. **Late descendant process leak window (medium).** Spawn cleanup snapshots
|
|
81
|
+
descendants at 3 seconds and then every 30 seconds. Children created and
|
|
82
|
+
reparented inside that window can evade final cleanup. Track descendants more
|
|
83
|
+
frequently while the runtime is alive.
|
|
84
|
+
6. **Supervisor stale PID reuse (medium).** Startup trusts any live PID in
|
|
85
|
+
`supervisor.pid`. Validate the command line against this checkout's
|
|
86
|
+
`engine/supervisor.js` before suppressing startup.
|
|
87
|
+
|
|
88
|
+
## Healthy areas
|
|
89
|
+
|
|
90
|
+
- Config/routing references and schedule schemas were valid.
|
|
91
|
+
- Runtime work-item and pull-request mirrors had no stuck dispatched rows,
|
|
92
|
+
duplicate PR IDs, or unknown PR states.
|
|
93
|
+
- No active dispatch was older than the five-hour wall-clock limit.
|
|
94
|
+
- Cooldown state was small and not exhibiting pending-context bloat.
|
package/docs/command-center.md
CHANGED
|
@@ -70,6 +70,8 @@ Any violation rejects the **whole turn** with a typed error envelope (`code: 'in
|
|
|
70
70
|
|
|
71
71
|
**Worker-pool interaction (W-mray463s001zcee2).** The opt-in Copilot ACP worker pool (`engine.ccUseWorkerPool: true`, default ON for Copilot CC) is a **separate protocol path** from the table above — `engine/cc-worker-pool.js`'s `session/prompt` call only ever sends a single text content block (`[{ type: 'text', text: prompt }]`) and has no image/attachment content-block support today. This used to mean image turns were silently dropped whenever the pool was active, with a comment incorrectly blaming a Copilot capability gap (`imageInput:false`) that doesn't exist — Copilot's `imageInput` is `true` and fully supports images on the direct spawn path. The fix: `_invokeCcStream` (dashboard.js) now checks for a non-empty `images` array **before** routing through `resolveCcUseWorkerPool` — image-bearing turns bypass the warm pool for that turn only and cold-spawn a one-off CLI process via the working `--attachment` path (same as the non-pooled path always used), while image-less turns keep using the fast warm pool. If `engine/cc-worker-pool.js`'s ACP session protocol is later extended to carry attachments (Option A), this per-turn bypass in `_invokeCcStream` should be revisited/removed.
|
|
72
72
|
|
|
73
|
+
Because that bypass can spend tens of seconds starting a one-off process before the first model token, the SSE stream shows a user-focused “Looking at the image…” status instead of exposing the cold-start implementation detail or leaving an apparently stalled spinner.
|
|
74
|
+
|
|
73
75
|
`_resolveImageOpts` (in `engine/llm.js`) is pure and unit-tested: it forwards the `images` list only when `runtime.capabilities.imageInput` is truthy, otherwise returns the typed error so CC/doc-chat surface the envelope instead of a silently-text-only reply. `_spawnProcess` re-applies the same gate (`if (!caps.imageInput) adapterOpts.images = undefined`) as defense in depth. Image **filenames** are run through the untrusted-input fence (`buildSource('cc-image-filename', …)`) before they reach prompt text, since they are user-supplied.
|
|
74
76
|
|
|
75
77
|
**No new `engine.*` flag.** The limits above are module-level constants in `dashboard.js` (`CC_IMAGE_MAX_COUNT`, `CC_IMAGE_MAX_DECODED_BYTES`, `CC_IMAGE_MIME_ALLOWLIST`), not config keys; `engine/shared.js` `ENGINE_DEFAULTS` was not touched. Per CLAUDE.md Best Practice #9 (Settings parity), no Settings toggle is added because no new `engine.*` flag was introduced. Runtime selection that determines whether images are accepted is the existing `engine.ccCli` / `ccModel` override, which already has a Settings control.
|
package/engine/lifecycle.js
CHANGED
|
@@ -1378,7 +1378,20 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1378
1378
|
// pr-links stays consistent (#779 pattern).
|
|
1379
1379
|
if (opts.itemId && Array.isArray(existing.prdItems) && !existing.prdItems.includes(opts.itemId)) {
|
|
1380
1380
|
try {
|
|
1381
|
-
|
|
1381
|
+
// W-mrduef3q000m2172 — derive targetPath from the scope the store
|
|
1382
|
+
// actually returned `existing` under (existing._scope), NOT from the
|
|
1383
|
+
// current call's `project` argument. When the two differ (e.g. the
|
|
1384
|
+
// record lives under 'central' but this enrollment call was made
|
|
1385
|
+
// with a specific project), re-deriving from `project` sends the
|
|
1386
|
+
// upsert to the WRONG scope's file/array. upsertPullRequestRecord's
|
|
1387
|
+
// tombstone guard then finds no matching record in that (wrong)
|
|
1388
|
+
// scope and falls through to insert-as-new, silently clearing any
|
|
1389
|
+
// userDeleted tombstone on the real record. See issue writeup for
|
|
1390
|
+
// the confirmed repro.
|
|
1391
|
+
const store = require('./pull-requests-store');
|
|
1392
|
+
const targetPath = existing._scope
|
|
1393
|
+
? store._filePathForScope(existing._scope)
|
|
1394
|
+
: (project ? shared.projectPrPath(project) : path.join(MINIONS_DIR, 'pull-requests.json'));
|
|
1382
1395
|
shared.upsertPullRequestRecord(targetPath, {
|
|
1383
1396
|
id: normalizedCanonicalId,
|
|
1384
1397
|
prNumber,
|
|
@@ -6861,6 +6874,15 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6861
6874
|
// a coding WI (not one of the validation types itself). Skips if the coding WI
|
|
6862
6875
|
// has no PR.
|
|
6863
6876
|
//
|
|
6877
|
+
// W-mrdq22j700056c6c — gated to real code-authoring completions only. Only
|
|
6878
|
+
// item.type in {implement, implement:large, fix} ever spawns a follow-up
|
|
6879
|
+
// validation WI. Every other work-item type (review, test, verify, setup,
|
|
6880
|
+
// explore, ask, docs, decompose, ...) is excluded even when it resolves a PR
|
|
6881
|
+
// ref and autoDispatch is on, because those completions don't author a code
|
|
6882
|
+
// diff worth re-validating — e.g. a `review` dispatch that merely comments on
|
|
6883
|
+
// someone else's PR previously spawned a spurious "Validate: Review opg PR
|
|
6884
|
+
// #792 ..." follow-up. See the ELIGIBLE_TYPES check below.
|
|
6885
|
+
//
|
|
6864
6886
|
// Files exactly ONE validation WI per coding-WI completion (W-mrhybval0001).
|
|
6865
6887
|
// `liveValidation.type` still governs CHECKOUT ROUTING — the set of work-item
|
|
6866
6888
|
// types that run in-place on the live checkout (see resolveCheckoutMode's
|
|
@@ -6880,6 +6902,10 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6880
6902
|
// own type is excluded so a validation-type WI can't recursively validate
|
|
6881
6903
|
// itself (anti-loop).
|
|
6882
6904
|
//
|
|
6905
|
+
// Only these item.type values ever trigger this hook (see ELIGIBLE_TYPES
|
|
6906
|
+
// below) — real code-authoring completions, not review/test/verify/setup/
|
|
6907
|
+
// explore/ask/docs/decompose completions.
|
|
6908
|
+
//
|
|
6883
6909
|
// Deduplicates per codingWiId: any non-terminal WI with
|
|
6884
6910
|
// meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
|
|
6885
6911
|
// validation task is ever in flight per coding completion.
|
|
@@ -6894,6 +6920,14 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6894
6920
|
// Validate: ..."), repeating indefinitely. Any WI that was itself created
|
|
6895
6921
|
// as a liveValidation follow-up (tagged via meta.liveValidationFor at
|
|
6896
6922
|
// creation, below) must never re-trigger this hook, regardless of its type.
|
|
6923
|
+
//
|
|
6924
|
+
// W-mrdq22j700056c6c — only real code-authoring work-item types ever spawn a
|
|
6925
|
+
// live-validation follow-up. Every other completed type (review, test,
|
|
6926
|
+
// verify, setup, explore, ask, docs, decompose, ...) is excluded even when it
|
|
6927
|
+
// resolves a PR ref, because those completions don't author a code diff
|
|
6928
|
+
// worth re-validating.
|
|
6929
|
+
const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([WORK_TYPE.IMPLEMENT, WORK_TYPE.IMPLEMENT_LARGE, WORK_TYPE.FIX]);
|
|
6930
|
+
|
|
6897
6931
|
function autoDispatchLiveValidationWi(meta, config) {
|
|
6898
6932
|
const item = meta?.item;
|
|
6899
6933
|
if (!item?.id || !item?.type) return;
|
|
@@ -6903,6 +6937,12 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6903
6937
|
// "Validate: Validate: ..." cascade (W-mr9u39az000db5c2 / issue #708).
|
|
6904
6938
|
if (item.meta && item.meta.liveValidationFor) return;
|
|
6905
6939
|
|
|
6940
|
+
// W-mrdq22j700056c6c — only implement/implement:large/fix completions ever
|
|
6941
|
+
// author a real code diff worth validating. Review, test, verify, setup,
|
|
6942
|
+
// explore, ask, docs, decompose, etc. completions must never spawn a
|
|
6943
|
+
// "Validate: ..." follow-up, even when they resolve a PR reference.
|
|
6944
|
+
if (!ELIGIBLE_LIVE_VALIDATION_TYPES.has(item.type)) return;
|
|
6945
|
+
|
|
6906
6946
|
// Issue #716 — QA Session infrastructure WIs (SETUP/DRAFT/EXECUTE, tagged
|
|
6907
6947
|
// via meta.sessionId and always dispatched with skipPr:true) never own a
|
|
6908
6948
|
// real code diff, so there is nothing for a live-validation WI to validate.
|
package/engine/playbook.js
CHANGED
|
@@ -1060,12 +1060,12 @@ function renderPlaybook(type, vars) {
|
|
|
1060
1060
|
// ─── Playbook Section Validator ──────────────────────────────────────────────
|
|
1061
1061
|
|
|
1062
1062
|
// Required structural section patterns — warn (do not throw) when absent.
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1063
|
+
// Playbooks use a few established names for the task-bearing section; accept
|
|
1064
|
+
// all of them rather than generating warn-level noise for valid templates.
|
|
1065
|
+
// '## Tools' / '## Constraints' were removed (#775) because no template
|
|
1066
|
+
// consistently implements those headers.
|
|
1067
1067
|
const _REQUIRED_PROMPT_SECTIONS = [
|
|
1068
|
-
{ pattern: /^## Your Task\b/m,
|
|
1068
|
+
{ pattern: /^## (?:Your Task|Task Description|Mission)\b/m, label: 'task section' },
|
|
1069
1069
|
];
|
|
1070
1070
|
|
|
1071
1071
|
/**
|
package/engine/process-utils.js
CHANGED
|
@@ -566,9 +566,9 @@ function findProcessCwdHolders(worktreePath, opts = {}) {
|
|
|
566
566
|
return _parseCwdHolderLines(raw, resolved);
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
-
// Cross-check a single PID's command line for a Minions agent invocation
|
|
570
|
-
//
|
|
571
|
-
//
|
|
569
|
+
// Cross-check a single PID's command line for a Minions agent invocation,
|
|
570
|
+
// including the shared spawn wrapper and every bundled runtime CLI. Used by
|
|
571
|
+
// orphan/recycled-PID safety:
|
|
572
572
|
// - engine/cleanup.js: gate before killing a PID found in engine/tmp/pid-*.pid
|
|
573
573
|
// - engine/timeout.js: gate before parking a dispatch as still-alive when
|
|
574
574
|
// the OS PID is alive but may belong to an unrelated recycled-PID process
|
|
@@ -578,9 +578,20 @@ function findProcessCwdHolders(worktreePath, opts = {}) {
|
|
|
578
578
|
// macOS / when /proc isn't available: fallback `ps -p <pid> -o command=`.
|
|
579
579
|
//
|
|
580
580
|
// Returns false when the PID is invalid, the process doesn't exist, the
|
|
581
|
-
// command line can't be read, or the cmdline contains
|
|
582
|
-
//
|
|
581
|
+
// command line can't be read, or the cmdline contains no recognized agent
|
|
582
|
+
// marker. False is the safe default for both call sites: cleanup falls
|
|
583
583
|
// through to "skip kill" and timeout falls through to "treat PID as dead".
|
|
584
|
+
const AGENT_COMMAND_PATTERNS = [
|
|
585
|
+
/(?:^|[\s"'\\/])(?:claude|copilot|codex)(?:\.exe|\.cmd|\.bat|\.js)?(?=$|[\s"])/i,
|
|
586
|
+
/[\\/]claude-code[\\/]cli\.js(?=$|[\s"])/i,
|
|
587
|
+
/[\\/]@github[\\/](?:copilot|copilot-cli)[\\/](?:(?:dist|bin)[\\/])?index\.js(?=$|[\s"])/i,
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
function _commandLineMatchesAgent(cmdline) {
|
|
591
|
+
const value = String(cmdline || '');
|
|
592
|
+
return AGENT_COMMAND_PATTERNS.some(pattern => pattern.test(value));
|
|
593
|
+
}
|
|
594
|
+
|
|
584
595
|
function isProcessCommandLineMatchingAgent(pid) {
|
|
585
596
|
const n = Number(pid);
|
|
586
597
|
if (!Number.isInteger(n) || n <= 0) return false;
|
|
@@ -605,8 +616,7 @@ function isProcessCommandLineMatchingAgent(pid) {
|
|
|
605
616
|
}
|
|
606
617
|
} catch { return false; }
|
|
607
618
|
if (!cmdline) return false;
|
|
608
|
-
|
|
609
|
-
return lower.includes('claude') || lower.includes('copilot');
|
|
619
|
+
return _commandLineMatchesAgent(cmdline);
|
|
610
620
|
}
|
|
611
621
|
|
|
612
622
|
function _buildChildMap(processes) {
|
|
@@ -693,6 +703,7 @@ module.exports = {
|
|
|
693
703
|
_parseCwdHolderLines,
|
|
694
704
|
findProcessCwdHolders,
|
|
695
705
|
isProcessCommandLineMatchingAgent,
|
|
706
|
+
_commandLineMatchesAgent, // exported for testing
|
|
696
707
|
_buildChildMap,
|
|
697
708
|
listProcessDescendants,
|
|
698
709
|
listProcessReachable,
|
package/engine/queries.js
CHANGED
|
@@ -995,12 +995,25 @@ function getPullRequests(config) {
|
|
|
995
995
|
// Then merge non-conflicting fields from the loser into the winner so we
|
|
996
996
|
// never lose prdItems, sourcePlan, itemType, minionsReview, or
|
|
997
997
|
// _automationFixCauses if only the loser carried them.
|
|
998
|
+
// Issue #803 — defense in depth: if a canonical PR id has a tombstoned
|
|
999
|
+
// (userDeleted:true) copy in ANY scope, treat the whole id as deleted, not
|
|
1000
|
+
// just that one record. Without this, a delete that (for whatever reason)
|
|
1001
|
+
// only tombstones one scope's copy would still have its untouched
|
|
1002
|
+
// cross-scope duplicate survive grouping below and get picked as the
|
|
1003
|
+
// dedupe "winner", silently resurrecting a PR the user just deleted.
|
|
1004
|
+
const tombstonedIds = new Set();
|
|
1005
|
+
for (const pr of sqlPrs) {
|
|
1006
|
+
if (pr && pr.id && pr.userDeleted === true) tombstonedIds.add(pr.id);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
998
1009
|
const groups = [];
|
|
999
1010
|
const groupsById = new Map();
|
|
1000
1011
|
for (const pr of sqlPrs) {
|
|
1001
1012
|
if (!pr || !pr.id) continue;
|
|
1002
|
-
// Issue #384: skip tombstoned records (user explicitly deleted)
|
|
1013
|
+
// Issue #384/#803: skip tombstoned records (user explicitly deleted), and
|
|
1014
|
+
// skip ANY record sharing an id that has a tombstoned copy elsewhere.
|
|
1003
1015
|
if (pr.userDeleted === true) continue;
|
|
1016
|
+
if (tombstonedIds.has(pr.id)) continue;
|
|
1004
1017
|
if (!groupsById.has(pr.id)) {
|
|
1005
1018
|
const g = [];
|
|
1006
1019
|
groupsById.set(pr.id, g);
|
|
@@ -3270,10 +3283,6 @@ function getStatusFastStateMtimePaths(config) {
|
|
|
3270
3283
|
* Claude CLI mutations (mcp add/remove, settings edits), not on every
|
|
3271
3284
|
* prompt, so whole-file tracking is acceptable.
|
|
3272
3285
|
*
|
|
3273
|
-
* Files intentionally NOT tracked here:
|
|
3274
|
-
* - version, autoMode, installId — change only on human/CLI edits, which
|
|
3275
|
-
* already pop the slow-state via reloadConfig + the 60 s TTL.
|
|
3276
|
-
*
|
|
3277
3286
|
* Per-project `.git/logs/HEAD` + `.git/FETCH_HEAD` ARE tracked here in
|
|
3278
3287
|
* addition to the fast-state tracker. The project payload (`projects:`
|
|
3279
3288
|
* with ahead/behind counts) lives in the slow-state slice, and
|
|
@@ -3314,6 +3323,14 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
3314
3323
|
// in the payload anymore.
|
|
3315
3324
|
const files = [];
|
|
3316
3325
|
|
|
3326
|
+
// Config-derived status fields (projects, autoMode, version settings) can be
|
|
3327
|
+
// changed by the CLI or another process without calling dashboard-local
|
|
3328
|
+
// invalidateStatusCache(). The unified cache has no TTL fallback, so these
|
|
3329
|
+
// source files must participate in the mtime registry.
|
|
3330
|
+
files.push(CONFIG_PATH);
|
|
3331
|
+
files.push(path.join(MINIONS_DIR, '.install-id'));
|
|
3332
|
+
files.push(path.join(MINIONS_DIR, 'package.json'));
|
|
3333
|
+
|
|
3317
3334
|
// Skill discovery roots (surfaced by _buildStatusSlowState → getSkills).
|
|
3318
3335
|
// Mirrors collectSkillFiles' source enumeration so adding a new runtime
|
|
3319
3336
|
// adapter automatically extends the tracker. ENOENT is tolerated by
|
package/engine/shared.js
CHANGED
|
@@ -7882,7 +7882,17 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7882
7882
|
...(Array.isArray(itemIds) ? itemIds : [itemId]),
|
|
7883
7883
|
]);
|
|
7884
7884
|
const prNumber = getPrNumber(entry.prNumber ?? entry.id ?? entry.url);
|
|
7885
|
-
|
|
7885
|
+
// Issue #802: when the caller already resolved a scope-qualified canonical
|
|
7886
|
+
// id (e.g. `ado:office/office/office#5370216`, as enrollPrFromCanonicalId
|
|
7887
|
+
// does before calling here), it must win over recomputing from a bare
|
|
7888
|
+
// entry.prNumber + project. Recomputing silently downgrades to a legacy
|
|
7889
|
+
// `PR-<n>` stub whenever project is null/unresolvable (getCanonicalPrId
|
|
7890
|
+
// can't derive a scope), discarding the good id even though it was passed
|
|
7891
|
+
// in. Prefer entry.id whenever it already parses as canonical.
|
|
7892
|
+
const hasCanonicalEntryId = typeof entry.id === 'string' && parseCanonicalPrId(entry.id) != null;
|
|
7893
|
+
const canonicalId = hasCanonicalEntryId
|
|
7894
|
+
? getCanonicalPrId(project, entry.id, entry.url || '')
|
|
7895
|
+
: getCanonicalPrId(project, entry.prNumber ?? entry.id ?? entry.url ?? prNumber, entry.url || '');
|
|
7886
7896
|
if (!canonicalId) throw new Error('PR id required');
|
|
7887
7897
|
const normalizedEntry = {
|
|
7888
7898
|
...entry,
|
|
@@ -7930,6 +7940,23 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7930
7940
|
delete target.userDeletedAt;
|
|
7931
7941
|
resurrected = true;
|
|
7932
7942
|
}
|
|
7943
|
+
// W-mrduef3q000m2172 — defense-in-depth: `prs` here is scoped to a single
|
|
7944
|
+
// scope's array (mutatePullRequests routes by prPath). If a caller
|
|
7945
|
+
// resolved the wrong scope for an already-tombstoned canonical id (the
|
|
7946
|
+
// record actually lives in a DIFFERENT scope's file), the above check
|
|
7947
|
+
// never sees it and would otherwise fall through to insert-as-new,
|
|
7948
|
+
// silently clearing the tombstone. Check globally before inserting.
|
|
7949
|
+
if (!target) {
|
|
7950
|
+
try {
|
|
7951
|
+
const store = require('./pull-requests-store');
|
|
7952
|
+
const globalMatch = (store.readAllPullRequests() || [])
|
|
7953
|
+
.find(pr => pr && pr.id === canonicalId && pr.userDeleted === true);
|
|
7954
|
+
if (globalMatch) {
|
|
7955
|
+
skipped = true;
|
|
7956
|
+
return prs;
|
|
7957
|
+
}
|
|
7958
|
+
} catch { /* best-effort global tombstone check — fall through on failure */ }
|
|
7959
|
+
}
|
|
7933
7960
|
// P-e9f0a2b4 — When findPrRecord returns null but multiple records share the
|
|
7934
7961
|
// same prNumber (e.g. cross-scope contamination or a project-config change
|
|
7935
7962
|
// that shifted canonical IDs), collapse the duplicates before inserting.
|
package/engine/worktree-gc.js
CHANGED
|
@@ -769,9 +769,9 @@ function pruneOrphanWorktrees(opts) {
|
|
|
769
769
|
const globalLiveDirNames = new Set();
|
|
770
770
|
for (const d of [...(dispatchSnap.active || []), ...(dispatchSnap.pending || [])]) {
|
|
771
771
|
if (!d || !d.id || !d.meta || !d.meta.branch) continue;
|
|
772
|
-
const dispatchProject =
|
|
772
|
+
const dispatchProject = typeof d.meta.project === 'string'
|
|
773
773
|
? d.meta.project
|
|
774
|
-
: 'default';
|
|
774
|
+
: (d.meta.project?.name || 'default');
|
|
775
775
|
try {
|
|
776
776
|
globalLiveDirNames.add(_buildWorktreeDirName({
|
|
777
777
|
dispatchId: d.id,
|
package/engine.js
CHANGED
|
@@ -28,13 +28,14 @@ require('./engine/stdio-timestamps').installIfNotInstalled();
|
|
|
28
28
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const path = require('path');
|
|
31
|
+
const os = require('os');
|
|
31
32
|
const crypto = require('crypto');
|
|
32
33
|
const shared = require('./engine/shared');
|
|
33
34
|
const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
34
35
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
|
|
35
36
|
FAILURE_CLASS, resolvePollFlag } = shared;
|
|
36
37
|
const { resolveRuntime } = require('./engine/runtimes');
|
|
37
|
-
const { assertStaleHeadOk } = require('./engine/spawn-agent');
|
|
38
|
+
const { assertStaleHeadOk, preApproveWorkspaceMcps, computeAddDirs } = require('./engine/spawn-agent');
|
|
38
39
|
const adoGitAuth = require('./engine/ado-git-auth');
|
|
39
40
|
const queries = require('./engine/queries');
|
|
40
41
|
const dispatchEvents = require('./engine/dispatch-events');
|
|
@@ -10140,7 +10141,9 @@ async function discoverWork(config) {
|
|
|
10140
10141
|
const activeMissionIds = new Set();
|
|
10141
10142
|
const activeScheduleIds = new Set();
|
|
10142
10143
|
for (const existing of items) {
|
|
10143
|
-
if (
|
|
10144
|
+
if (DONE_STATUSES.has(existing.status) ||
|
|
10145
|
+
existing.status === WI_STATUS.FAILED ||
|
|
10146
|
+
existing.status === WI_STATUS.CANCELLED) continue;
|
|
10144
10147
|
if (existing._missionId) activeMissionIds.add(existing._missionId);
|
|
10145
10148
|
if (existing._scheduleId) activeScheduleIds.add(existing._scheduleId);
|
|
10146
10149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2364",
|
|
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"
|