@yemi33/minions 0.1.956 → 0.1.958
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/CHANGELOG.md +5 -5
- package/docs/auto-discovery.md +1 -1
- package/engine/playbook.js +19 -2
- package/engine/queries.js +36 -2
- package/engine/shared.js +1 -0
- package/engine.js +59 -3
- package/package.json +1 -1
- package/playbooks/shared-rules.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.958 (2026-04-14)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- surface in-flight tool calls in lastAction (#1064)
|
|
6
7
|
- wire agentBusyReassignMs into settings UI and persist
|
|
7
8
|
- ADO throttle detection, poll guards, and dashboard banner (#1051)
|
|
8
9
|
- gate auto-fix conflict dispatch behind autoFixConflicts flag
|
|
@@ -22,9 +23,11 @@
|
|
|
22
23
|
- replace magic string 'active' with PR_STATUS.ACTIVE in lifecycle.js (#969)
|
|
23
24
|
- fix dashboard plan-pause nested lock violation (#968)
|
|
24
25
|
- add missing branch_name to central dispatch vars (#967)
|
|
25
|
-
- fix dispatch.js mutator fallback to use nullish coalescing (#966)
|
|
26
26
|
|
|
27
27
|
### Fixes
|
|
28
|
+
- enforce --timeout 1 on all azureauth calls to prevent agent orphans (#1063)
|
|
29
|
+
- cancel steering kill watcher on resume spawn (#1052) (#1062)
|
|
30
|
+
- suppress warn for optional template variables (#1061)
|
|
28
31
|
- fix ENGINE_DEFAULTS ref and derive boolean settings from defaults
|
|
29
32
|
- add autoFixConflicts to boolean settings persist list
|
|
30
33
|
- tighten build query guard to require mergeCommitId
|
|
@@ -42,9 +45,6 @@
|
|
|
42
45
|
- steering kill on no-session re-queues dispatch instead of erroring (#1015)
|
|
43
46
|
- inject cached ADO token into spawned agents (#998) (#1012)
|
|
44
47
|
- qaAbort race + skip kill on completed doc-chat
|
|
45
|
-
- doc-chat Stop button actually works — release server guard on disconnect
|
|
46
|
-
- CC streaming auto-retries fresh session when resume fails
|
|
47
|
-
- 429 retry on abort race applies to all sends, not just queued
|
|
48
48
|
|
|
49
49
|
### Other
|
|
50
50
|
- test(engine): add regression tests for autoFixConflicts flag and DEFAULTS alias
|
package/docs/auto-discovery.md
CHANGED
|
@@ -137,7 +137,7 @@ The engine directly polls the Azure DevOps REST API for **all** PR metadata: bui
|
|
|
137
137
|
| `buildStatus` | PR statuses (codecoverage/deploy/build/ci contexts) | `passing` / `failing` / `running` / `none` |
|
|
138
138
|
| `buildFailReason` | Failed status description | Set on failure, cleared otherwise |
|
|
139
139
|
|
|
140
|
-
**Auth:** Bearer token via `azureauth ado token --output token` (cached 30 minutes).
|
|
140
|
+
**Auth:** Bearer token via `azureauth ado token --output token --timeout 1` (cached 30 minutes). The `--timeout 1` flag is required — without it, azureauth can hang indefinitely in headless sessions.
|
|
141
141
|
|
|
142
142
|
This feeds `discoverFromPrs` — when `buildStatus` flips to `"failing"`, the next discovery tick dispatches a fix agent. When `status` becomes `"merged"`, the PR drops out of active polling.
|
|
143
143
|
|
package/engine/playbook.js
CHANGED
|
@@ -214,6 +214,22 @@ function resolveTaskContext(item, config) {
|
|
|
214
214
|
// are optional by design. Only variables that make the playbook non-functional
|
|
215
215
|
// when absent are listed here.
|
|
216
216
|
|
|
217
|
+
// ─── Optional Template Variables ────────────────────────────────────────────
|
|
218
|
+
// Variables that legitimately resolve to empty string for ad-hoc work items.
|
|
219
|
+
// These are silently filtered out of the empty-string warning to avoid
|
|
220
|
+
// masking real warnings. Add new optional vars here when they are contextual
|
|
221
|
+
// (plan-linked, checkpoint-dependent, etc.) and not required for the playbook
|
|
222
|
+
// to function.
|
|
223
|
+
|
|
224
|
+
const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
225
|
+
'source_plan', // only set when work item is linked to a plan
|
|
226
|
+
'plan_slug', // derived from source_plan
|
|
227
|
+
'additional_context', // only set when item has a prompt
|
|
228
|
+
'references', // only set when item.references has entries
|
|
229
|
+
'acceptance_criteria', // only set when item.acceptanceCriteria has entries
|
|
230
|
+
'checkpoint_context', // only set when resuming from a prior timeout
|
|
231
|
+
]);
|
|
232
|
+
|
|
217
233
|
const PLAYBOOK_REQUIRED_VARS = {
|
|
218
234
|
'implement': ['item_id', 'item_name', 'branch_name', 'project_path'],
|
|
219
235
|
'implement-shared': ['item_id', 'item_name', 'branch_name', 'worktree_path'],
|
|
@@ -364,9 +380,9 @@ function renderPlaybook(type, vars) {
|
|
|
364
380
|
log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
|
|
365
381
|
}
|
|
366
382
|
|
|
367
|
-
// Warn on variables that resolved to empty string
|
|
383
|
+
// Warn on variables that resolved to empty string (skip known-optional vars)
|
|
368
384
|
const emptyVars = Object.entries(allVars)
|
|
369
|
-
.filter(([, val]) => String(val) === '')
|
|
385
|
+
.filter(([key, val]) => String(val) === '' && !PLAYBOOK_OPTIONAL_VARS.has(key))
|
|
370
386
|
.map(([key]) => key);
|
|
371
387
|
if (emptyVars.length > 0) {
|
|
372
388
|
log('warn', `Playbook "${type}": template variables resolved to empty string: ${emptyVars.join(', ')}`);
|
|
@@ -551,6 +567,7 @@ module.exports = {
|
|
|
551
567
|
renderPlaybook,
|
|
552
568
|
validatePlaybookVars,
|
|
553
569
|
PLAYBOOK_REQUIRED_VARS,
|
|
570
|
+
PLAYBOOK_OPTIONAL_VARS,
|
|
554
571
|
buildSystemPrompt,
|
|
555
572
|
buildAgentContext,
|
|
556
573
|
selectPlaybook,
|
package/engine/queries.js
CHANGED
|
@@ -42,6 +42,34 @@ function readHeadTail(filePath, bytes = 1024) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Detect in-flight tool calls from live-output.log tail content.
|
|
47
|
+
* Scans for task_started events with no matching task_notification (by task_id).
|
|
48
|
+
* Returns { description, taskId } for the most recent in-flight tool, or null.
|
|
49
|
+
*/
|
|
50
|
+
function detectInFlightTool(tail) {
|
|
51
|
+
if (!tail) return null;
|
|
52
|
+
const lines = tail.split('\n');
|
|
53
|
+
const completed = new Set();
|
|
54
|
+
|
|
55
|
+
// Reverse scan: collect task_notification ids first (most recent lines),
|
|
56
|
+
// then the first task_started not in completed is the in-flight tool.
|
|
57
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
if (!line.includes('"task_')) continue; // fast skip non-task lines
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(line);
|
|
62
|
+
if (parsed.type !== 'system') continue;
|
|
63
|
+
if (parsed.subtype === 'task_notification') {
|
|
64
|
+
completed.add(parsed.task_id);
|
|
65
|
+
} else if (parsed.subtype === 'task_started' && !completed.has(parsed.task_id)) {
|
|
66
|
+
return { description: parsed.description || null, taskId: parsed.task_id };
|
|
67
|
+
}
|
|
68
|
+
} catch { /* not valid JSON — skip heartbeats, headers, partial lines */ }
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
46
74
|
|
|
47
75
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -220,7 +248,7 @@ function getAgentStatus(agentId) {
|
|
|
220
248
|
if (active._blockingToolCall) {
|
|
221
249
|
result._blockingToolCall = active._blockingToolCall;
|
|
222
250
|
}
|
|
223
|
-
// Detect permission-waiting: read only head+tail of live-output.log (max 2KB total)
|
|
251
|
+
// Detect permission-waiting and in-flight tools: read only head+tail of live-output.log (max 2KB total)
|
|
224
252
|
try {
|
|
225
253
|
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
226
254
|
const { head, tail } = readHeadTail(liveLogPath, 1024);
|
|
@@ -239,6 +267,11 @@ function getAgentStatus(agentId) {
|
|
|
239
267
|
result._warning = 'Possibly waiting for permission approval — agent is not in bypass mode';
|
|
240
268
|
}
|
|
241
269
|
}
|
|
270
|
+
// Detect in-flight tool calls (task_started with no task_notification)
|
|
271
|
+
const inFlight = detectInFlightTool(tail);
|
|
272
|
+
if (inFlight && inFlight.description) {
|
|
273
|
+
result._runningToolDescription = inFlight.description;
|
|
274
|
+
}
|
|
242
275
|
}
|
|
243
276
|
} catch { /* optional — don't block status */ }
|
|
244
277
|
return result;
|
|
@@ -328,7 +361,7 @@ function getAgents(config) {
|
|
|
328
361
|
const s = getAgentStatus(a.id); // derives from dispatch.json
|
|
329
362
|
|
|
330
363
|
let lastAction = 'Waiting for assignment';
|
|
331
|
-
if (s.status === 'working') lastAction = `Working: ${s.task}`;
|
|
364
|
+
if (s.status === 'working') lastAction = s._runningToolDescription ? `Running: ${s._runningToolDescription}` : `Working: ${s.task}`;
|
|
332
365
|
else if (s.status === 'done') lastAction = `Done: ${s.task}`;
|
|
333
366
|
else if (s.status === 'error') lastAction = `Error: ${s.task}`;
|
|
334
367
|
else if (inboxFiles.length > 0) {
|
|
@@ -1033,6 +1066,7 @@ module.exports = {
|
|
|
1033
1066
|
// Helpers
|
|
1034
1067
|
timeSince,
|
|
1035
1068
|
readHeadTail, // exported for testing
|
|
1069
|
+
detectInFlightTool, // exported for testing
|
|
1036
1070
|
resetPrdInfoCache,
|
|
1037
1071
|
|
|
1038
1072
|
// Core state
|
package/engine/shared.js
CHANGED
|
@@ -555,6 +555,7 @@ const ENGINE_DEFAULTS = {
|
|
|
555
555
|
autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
|
|
556
556
|
prMergeMethod: 'squash', // merge method: squash, merge, rebase
|
|
557
557
|
ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
|
|
558
|
+
agentBusyReassignMs: 600000, // 10min — reassign work item to another agent if preferred agent is busy beyond this threshold
|
|
558
559
|
ccModel: 'sonnet', // model for Command Center and doc-chat (sonnet, haiku, opus)
|
|
559
560
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
560
561
|
heartbeatTimeouts: {}, // populated after WORK_TYPE is defined (below)
|
package/engine.js
CHANGED
|
@@ -1051,8 +1051,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1051
1051
|
return;
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
-
// Re-attach to existing tracking
|
|
1055
|
-
|
|
1054
|
+
// Re-attach to existing tracking — do NOT carry _steeringAt forward (#1052).
|
|
1055
|
+
// The kill watcher in timeout.js fires 30s after _steeringAt is set. If we carry it
|
|
1056
|
+
// into the resumed process, it kills the resumed session. The kill watcher only exists
|
|
1057
|
+
// to handle cases where the original kill didn't take effect — once the process has
|
|
1058
|
+
// exited and the resume is spawned, _steeringAt must not be present.
|
|
1059
|
+
activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, lastRealOutputAt: Date.now() });
|
|
1056
1060
|
|
|
1057
1061
|
// Reset output buffers so post-completion parsing only sees the resumed session
|
|
1058
1062
|
stdout = '';
|
|
@@ -1300,6 +1304,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1300
1304
|
const item = dispatch.pending.splice(idx, 1)[0];
|
|
1301
1305
|
item.started_at = startedAt;
|
|
1302
1306
|
delete item.skipReason;
|
|
1307
|
+
delete item._agentBusySince;
|
|
1303
1308
|
if (!dispatch.active.some(d => d.id === id)) {
|
|
1304
1309
|
dispatch.active.push(item);
|
|
1305
1310
|
}
|
|
@@ -3324,7 +3329,46 @@ async function tickInner() {
|
|
|
3324
3329
|
log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
|
|
3325
3330
|
continue;
|
|
3326
3331
|
}
|
|
3327
|
-
if (busyAgents.has(item.agent))
|
|
3332
|
+
if (busyAgents.has(item.agent)) {
|
|
3333
|
+
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3334
|
+
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3335
|
+
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3336
|
+
const isExplicitReassign = !!item.meta?.item?.agent;
|
|
3337
|
+
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3338
|
+
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
3339
|
+
if (Date.now() - busySinceMs > reassignMs) {
|
|
3340
|
+
const originalAgent = item.agent;
|
|
3341
|
+
const altAgent = resolveAgent(item.type, config);
|
|
3342
|
+
if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
|
|
3343
|
+
log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy > ${reassignMs}ms`);
|
|
3344
|
+
item.agent = altAgent;
|
|
3345
|
+
item.agentName = config.agents[altAgent]?.name || tempAgents.get(altAgent)?.name || altAgent;
|
|
3346
|
+
item.agentRole = config.agents[altAgent]?.role || tempAgents.get(altAgent)?.role || 'Agent';
|
|
3347
|
+
delete item._agentBusySince;
|
|
3348
|
+
delete item.skipReason;
|
|
3349
|
+
// Persist reassignment to dispatch.json
|
|
3350
|
+
mutateDispatch((dp) => {
|
|
3351
|
+
const p = (dp.pending || []).find(d => d.id === item.id);
|
|
3352
|
+
if (p) {
|
|
3353
|
+
p.agent = altAgent;
|
|
3354
|
+
p.agentName = item.agentName;
|
|
3355
|
+
p.agentRole = item.agentRole;
|
|
3356
|
+
delete p._agentBusySince;
|
|
3357
|
+
delete p.skipReason;
|
|
3358
|
+
}
|
|
3359
|
+
return dp;
|
|
3360
|
+
});
|
|
3361
|
+
// Fall through to branch mutex / concurrency checks below
|
|
3362
|
+
} else {
|
|
3363
|
+
continue; // No alternative agent available — keep waiting
|
|
3364
|
+
}
|
|
3365
|
+
} else {
|
|
3366
|
+
continue; // Below threshold — keep waiting
|
|
3367
|
+
}
|
|
3368
|
+
} else {
|
|
3369
|
+
continue; // No _agentBusySince set yet or explicitly assigned — skip
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3328
3372
|
// Branch mutex: skip items targeting a branch already locked by an active or newly-dispatched task
|
|
3329
3373
|
const itemBranch = item.meta?.branch ? sanitizeBranch(item.meta.branch) : null;
|
|
3330
3374
|
if (itemBranch && lockedBranches.has(itemBranch)) continue;
|
|
@@ -3412,6 +3456,18 @@ async function tickInner() {
|
|
|
3412
3456
|
reason = 'branch_locked';
|
|
3413
3457
|
}
|
|
3414
3458
|
}
|
|
3459
|
+
// Track when item first became blocked on a busy agent for reassignment threshold
|
|
3460
|
+
if (reason === 'agent_busy') {
|
|
3461
|
+
if (!item._agentBusySince) {
|
|
3462
|
+
item._agentBusySince = ts();
|
|
3463
|
+
skipReasonChanged = true;
|
|
3464
|
+
}
|
|
3465
|
+
} else {
|
|
3466
|
+
if (item._agentBusySince) {
|
|
3467
|
+
delete item._agentBusySince;
|
|
3468
|
+
skipReasonChanged = true;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3415
3471
|
if (item.skipReason !== reason) {
|
|
3416
3472
|
item.skipReason = reason;
|
|
3417
3473
|
skipReasonChanged = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.958",
|
|
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"
|
|
@@ -54,3 +54,5 @@ node engine/ado-status.js <prNumber> --live --project MyProject
|
|
|
54
54
|
Output is JSON with the same fields. Exit 0 on success, 1 if not found.
|
|
55
55
|
|
|
56
56
|
**Never make raw `curl` calls to ADO APIs directly.** Use `node engine/ado-status.js` which routes through `ado.js` — authenticated, retried, circuit-broken. Raw `azureauth` + curl bypasses all of that.
|
|
57
|
+
|
|
58
|
+
**If you must run `azureauth` directly, ALWAYS include `--timeout 1`.** Without this flag, `azureauth ado token` can hang indefinitely waiting for interactive broker UI that never appears in headless agent sessions. This causes the Claude Code process to silently exit and the engine to declare the agent orphaned. Example: `azureauth ado token --mode iwa --mode broker --output token --timeout 1`.
|