@yemi33/minions 0.1.7 → 0.1.9
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 +46 -0
- package/README.md +60 -2
- package/bin/minions.js +51 -2
- package/config.template.json +1 -25
- package/dashboard.html +106 -3
- package/dashboard.js +101 -0
- package/docs/auto-discovery.md +2 -2
- package/engine/ado.js +1 -0
- package/engine/cli.js +120 -6
- package/engine/github.js +1 -0
- package/engine/lifecycle.js +86 -0
- package/engine/preflight.js +239 -0
- package/engine/scheduler.js +159 -0
- package/engine/shared.js +4 -1
- package/engine/spawn-agent.js +4 -2
- package/engine.js +167 -15
- package/package.json +1 -1
- package/playbooks/decompose.md +60 -0
- package/routing.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.9 (2026-03-26)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
- engine/ado.js
|
|
8
|
+
- engine/cli.js
|
|
9
|
+
- engine/github.js
|
|
10
|
+
- engine/lifecycle.js
|
|
11
|
+
- engine/scheduler.js
|
|
12
|
+
- engine/shared.js
|
|
13
|
+
|
|
14
|
+
### Dashboard
|
|
15
|
+
- dashboard.html
|
|
16
|
+
- dashboard.js
|
|
17
|
+
|
|
18
|
+
### Playbooks
|
|
19
|
+
- decompose.md
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
- README.md
|
|
23
|
+
|
|
24
|
+
### Other
|
|
25
|
+
- CLAUDE.md
|
|
26
|
+
- TODO.md
|
|
27
|
+
- routing.md
|
|
28
|
+
- test/playwright/dashboard.spec.js
|
|
29
|
+
|
|
30
|
+
## 0.1.8 (2026-03-25)
|
|
31
|
+
|
|
32
|
+
### Engine
|
|
33
|
+
- engine.js
|
|
34
|
+
- engine/cli.js
|
|
35
|
+
- engine/preflight.js
|
|
36
|
+
- engine/shared.js
|
|
37
|
+
- engine/spawn-agent.js
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
- README.md
|
|
41
|
+
- auto-discovery.md
|
|
42
|
+
|
|
43
|
+
### Other
|
|
44
|
+
- CLAUDE.md
|
|
45
|
+
- bin/minions.js
|
|
46
|
+
- config.template.json
|
|
47
|
+
- test/unit.test.js
|
|
48
|
+
|
|
3
49
|
## 0.1.7 (2026-03-24)
|
|
4
50
|
|
|
5
51
|
### Documentation
|
package/README.md
CHANGED
|
@@ -473,7 +473,7 @@ Engine behavior is controlled via `config.json`. Key settings:
|
|
|
473
473
|
{
|
|
474
474
|
"engine": {
|
|
475
475
|
"tickInterval": 60000,
|
|
476
|
-
"maxConcurrent":
|
|
476
|
+
"maxConcurrent": 5,
|
|
477
477
|
"agentTimeout": 18000000,
|
|
478
478
|
"heartbeatTimeout": 300000,
|
|
479
479
|
"maxTurns": 100,
|
|
@@ -487,7 +487,7 @@ Engine behavior is controlled via `config.json`. Key settings:
|
|
|
487
487
|
| Setting | Default | Description |
|
|
488
488
|
|---------|---------|-------------|
|
|
489
489
|
| `tickInterval` | 60000 (1min) | Milliseconds between engine ticks |
|
|
490
|
-
| `maxConcurrent` |
|
|
490
|
+
| `maxConcurrent` | 5 | Max agents running simultaneously |
|
|
491
491
|
| `agentTimeout` | 18000000 (5h) | Max total agent runtime |
|
|
492
492
|
| `heartbeatTimeout` | 300000 (5min) | Kill agents silent longer than this |
|
|
493
493
|
| `maxTurns` | 100 | Max Claude CLI turns per agent session |
|
|
@@ -497,6 +497,60 @@ Engine behavior is controlled via `config.json`. Key settings:
|
|
|
497
497
|
| `worktreeRoot` | `../worktrees` | Where git worktrees are created |
|
|
498
498
|
| `idleAlertMinutes` | 15 | Alert after no dispatch for this many minutes |
|
|
499
499
|
| `restartGracePeriod` | 1200000 (20min) | Grace period for agent re-attachment after engine restart |
|
|
500
|
+
| `shutdownTimeout` | 300000 (5min) | Max wait for active agents during graceful shutdown (SIGTERM/SIGINT) |
|
|
501
|
+
| `allowTempAgents` | false | Spawn ephemeral agents when all permanent agents are busy |
|
|
502
|
+
| `autoDecompose` | true | Auto-decompose `implement:large` items into sub-tasks before dispatch |
|
|
503
|
+
|
|
504
|
+
### Scheduled Tasks
|
|
505
|
+
|
|
506
|
+
Add recurring work via `config.schedules`:
|
|
507
|
+
|
|
508
|
+
```json
|
|
509
|
+
{
|
|
510
|
+
"schedules": [
|
|
511
|
+
{
|
|
512
|
+
"id": "nightly-tests",
|
|
513
|
+
"cron": "0 2 *",
|
|
514
|
+
"type": "test",
|
|
515
|
+
"title": "Nightly test suite",
|
|
516
|
+
"project": "MyProject",
|
|
517
|
+
"agent": "dallas",
|
|
518
|
+
"enabled": true
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Cron format is simplified 3-field: `minute hour dayOfWeek` (0=Sun..6=Sat). Supports `*`, `*/N`, and specific values. Examples:
|
|
525
|
+
- `0 2 *` — 2am daily
|
|
526
|
+
- `0 9 1` — 9am every Monday
|
|
527
|
+
- `*/30 * *` — every 30 minutes
|
|
528
|
+
- `0 9 1,3,5` — 9am Mon/Wed/Fri
|
|
529
|
+
|
|
530
|
+
### Graceful Shutdown
|
|
531
|
+
|
|
532
|
+
The engine handles `SIGTERM` and `SIGINT` (Ctrl+C) gracefully:
|
|
533
|
+
1. Stops accepting new work (enters `stopping` state)
|
|
534
|
+
2. Waits for active agents to finish (up to `shutdownTimeout`, default 5 minutes)
|
|
535
|
+
3. Exits cleanly
|
|
536
|
+
|
|
537
|
+
Active agents continue running as independent processes and will be re-attached on next engine start.
|
|
538
|
+
|
|
539
|
+
### Task Decomposition
|
|
540
|
+
|
|
541
|
+
Work items with `complexity: "large"` or `estimated_complexity: "large"` are auto-decomposed before dispatch (controlled by `engine.autoDecompose`, default `true`). The engine dispatches a `decompose` agent that breaks the item into 2-5 smaller sub-tasks, each becoming an independent work item with dependency tracking.
|
|
542
|
+
|
|
543
|
+
### Temporary Agents
|
|
544
|
+
|
|
545
|
+
Set `engine.allowTempAgents: true` to let the engine spawn ephemeral agents when all 5 permanent agents are busy. Temp agents:
|
|
546
|
+
- Get a `temp-{id}` identifier
|
|
547
|
+
- Use a minimal system prompt (no charter)
|
|
548
|
+
- Are auto-cleaned up after task completion
|
|
549
|
+
- Count toward `maxConcurrent` slots
|
|
550
|
+
|
|
551
|
+
### Live Output Streaming
|
|
552
|
+
|
|
553
|
+
The dashboard streams agent output in real-time via Server-Sent Events (SSE) instead of polling. The `GET /api/agent/:id/live-stream` endpoint pushes output chunks as they're written. Falls back to 3-second polling if SSE is unavailable.
|
|
500
554
|
|
|
501
555
|
## Node.js Upgrade Caution
|
|
502
556
|
|
|
@@ -535,6 +589,8 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
|
|
|
535
589
|
ado.js <- ADO token management, PR polling, PR reconciliation
|
|
536
590
|
llm.js <- callLLM() with session resume, trackEngineUsage()
|
|
537
591
|
spawn-agent.js <- Agent spawn wrapper (resolves claude cli.js)
|
|
592
|
+
preflight.js <- Prerequisite checks (Node, Git, Claude CLI, API key)
|
|
593
|
+
scheduler.js <- Cron-style scheduled task discovery
|
|
538
594
|
ado-mcp-wrapper.js <- ADO MCP authentication wrapper
|
|
539
595
|
check-status.js <- Quick status check without full engine load
|
|
540
596
|
control.json <- running/paused/stopped (runtime, generated)
|
|
@@ -542,6 +598,7 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
|
|
|
542
598
|
log.json <- Audit trail, capped at 500 (runtime, generated)
|
|
543
599
|
metrics.json <- Per-agent quality metrics (runtime, generated)
|
|
544
600
|
cooldowns.json <- Dispatch cooldown tracking (runtime, generated)
|
|
601
|
+
schedule-runs.json <- Last-run timestamps for scheduled tasks (runtime, generated)
|
|
545
602
|
dashboard.js <- Web dashboard server
|
|
546
603
|
dashboard.html <- Dashboard UI (single-file)
|
|
547
604
|
config.json <- projects[], agents, engine, claude settings (generated by minions init)
|
|
@@ -566,6 +623,7 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
|
|
|
566
623
|
implement-shared.md <- Implement on a shared branch
|
|
567
624
|
ask.md <- Answer a question about the codebase
|
|
568
625
|
verify.md <- Plan verification: build, test, start webapp, testing guide
|
|
626
|
+
decompose.md <- Break large work items into 2-5 sub-tasks
|
|
569
627
|
skills/ <- Agent-created reusable workflows (generated)
|
|
570
628
|
agents/
|
|
571
629
|
{name}/
|
package/bin/minions.js
CHANGED
|
@@ -297,6 +297,13 @@ function init() {
|
|
|
297
297
|
showChangelog(installedVersion);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// Run preflight checks (warn only — don't block init)
|
|
301
|
+
try {
|
|
302
|
+
const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
303
|
+
const { results } = runPreflight();
|
|
304
|
+
printPreflight(results, { label: 'Preflight checks' });
|
|
305
|
+
} catch {}
|
|
306
|
+
|
|
300
307
|
// Auto-start on fresh install; force-upgrade restarts automatically.
|
|
301
308
|
if (isUpgrade) {
|
|
302
309
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
@@ -315,7 +322,17 @@ function init() {
|
|
|
315
322
|
});
|
|
316
323
|
dashProc.unref();
|
|
317
324
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
318
|
-
console.log(' Dashboard: http://localhost:7331
|
|
325
|
+
console.log(' Dashboard: http://localhost:7331');
|
|
326
|
+
|
|
327
|
+
// Next steps guidance
|
|
328
|
+
console.log(`
|
|
329
|
+
Next steps:
|
|
330
|
+
minions work "Explore the codebase" Give your first task
|
|
331
|
+
minions status Check engine state
|
|
332
|
+
http://localhost:7331 Open the dashboard
|
|
333
|
+
minions doctor Verify everything is working
|
|
334
|
+
minions --help See all commands
|
|
335
|
+
`);
|
|
319
336
|
}
|
|
320
337
|
|
|
321
338
|
function copyDir(src, dest, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions, relPath = '') {
|
|
@@ -391,6 +408,16 @@ function showVersion() {
|
|
|
391
408
|
} else {
|
|
392
409
|
console.log(' Not installed yet. Run: minions init');
|
|
393
410
|
}
|
|
411
|
+
|
|
412
|
+
// Check npm registry for latest version (best-effort, non-blocking)
|
|
413
|
+
try {
|
|
414
|
+
const latest = execSync('npm view @yemi33/minions version', { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim();
|
|
415
|
+
if (latest && latest !== pkg) {
|
|
416
|
+
console.log(`\n Latest on npm: ${latest}`);
|
|
417
|
+
console.log(' To update: npm update -g @yemi33/minions && minions init --force');
|
|
418
|
+
}
|
|
419
|
+
} catch {} // offline or npm not available — skip silently
|
|
420
|
+
|
|
394
421
|
console.log('');
|
|
395
422
|
}
|
|
396
423
|
|
|
@@ -428,12 +455,14 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
428
455
|
minions init [--skip-scan] Bootstrap ~/.minions/ (first time)
|
|
429
456
|
minions init --force Upgrade engine code + add new files (auto-skip scan)
|
|
430
457
|
minions version Show installed vs package version
|
|
458
|
+
minions doctor Check prerequisites and runtime health
|
|
431
459
|
minions add <project-dir> Link a project (interactive)
|
|
432
460
|
minions remove <project-dir> Unlink a project
|
|
433
461
|
minions list List linked projects
|
|
434
462
|
|
|
435
463
|
Engine:
|
|
436
|
-
minions
|
|
464
|
+
minions up Start engine + dashboard (use after reboot)
|
|
465
|
+
minions start Start engine daemon only
|
|
437
466
|
minions stop Stop the engine
|
|
438
467
|
minions status Show agents, projects, queue
|
|
439
468
|
minions pause / resume Pause/resume dispatching
|
|
@@ -455,6 +484,26 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
455
484
|
showVersion();
|
|
456
485
|
} else if (cmd === 'add' || cmd === 'remove' || cmd === 'list') {
|
|
457
486
|
delegate('minions.js', [cmd, ...rest]);
|
|
487
|
+
} else if (cmd === 'up' || cmd === 'restart') {
|
|
488
|
+
// Start both engine and dashboard — the go-to command after a reboot
|
|
489
|
+
ensureInstalled();
|
|
490
|
+
// Stop engine if running (ignore errors)
|
|
491
|
+
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
492
|
+
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
493
|
+
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
494
|
+
});
|
|
495
|
+
engineProc.unref();
|
|
496
|
+
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
497
|
+
const dashProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
498
|
+
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
499
|
+
});
|
|
500
|
+
dashProc.unref();
|
|
501
|
+
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
502
|
+
console.log(' Dashboard: http://localhost:7331\n');
|
|
503
|
+
} else if (cmd === 'doctor') {
|
|
504
|
+
ensureInstalled();
|
|
505
|
+
const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
506
|
+
doctor(MINIONS_HOME).then(ok => process.exit(ok ? 0 : 1));
|
|
458
507
|
} else if (cmd === 'dash' || cmd === 'dashboard') {
|
|
459
508
|
delegate('dashboard.js', rest);
|
|
460
509
|
} else if (engineCmds.has(cmd)) {
|
package/config.template.json
CHANGED
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"projects": [
|
|
3
|
-
{
|
|
4
|
-
"name": "YOUR_PROJECT_NAME",
|
|
5
|
-
"localPath": "/path/to/your/project",
|
|
6
|
-
"repoHost": "ado",
|
|
7
|
-
"repositoryId": "YOUR_REPO_ID",
|
|
8
|
-
"adoOrg": "YOUR_ORG",
|
|
9
|
-
"adoProject": "YOUR_PROJECT",
|
|
10
|
-
"repoName": "YOUR_REPO_NAME",
|
|
11
|
-
"mainBranch": "main",
|
|
12
|
-
"prUrlBase": "",
|
|
13
|
-
"workSources": {
|
|
14
|
-
"pullRequests": {
|
|
15
|
-
"enabled": true,
|
|
16
|
-
"path": ".minions/pull-requests.json",
|
|
17
|
-
"cooldownMinutes": 30
|
|
18
|
-
},
|
|
19
|
-
"workItems": {
|
|
20
|
-
"enabled": true,
|
|
21
|
-
"path": ".minions/work-items.json",
|
|
22
|
-
"cooldownMinutes": 0
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
]
|
|
2
|
+
"projects": []
|
|
27
3
|
}
|
|
28
4
|
|
package/dashboard.html
CHANGED
|
@@ -917,7 +917,7 @@ function closeDetail() {
|
|
|
917
917
|
document.getElementById('detail-overlay').classList.remove('open');
|
|
918
918
|
document.getElementById('detail-panel').classList.remove('open');
|
|
919
919
|
currentAgentId = null;
|
|
920
|
-
|
|
920
|
+
stopLiveStream();
|
|
921
921
|
}
|
|
922
922
|
|
|
923
923
|
function renderDetailTabs(detail) {
|
|
@@ -978,10 +978,10 @@ function renderDetailContent(detail, tab) {
|
|
|
978
978
|
} else if (tab === 'live') {
|
|
979
979
|
el.innerHTML = '<div class="section" id="live-output" style="max-height:60vh;overflow-y:auto;font-size:11px;line-height:1.6">Loading live output...</div>' +
|
|
980
980
|
'<div style="margin-top:8px;display:flex;gap:8px;align-items:center">' +
|
|
981
|
-
'<span class="pulse"></span><span style="font-size:11px;color:var(--green)">
|
|
981
|
+
'<span class="pulse"></span><span id="live-status-label" style="font-size:11px;color:var(--green)">Streaming live</span>' +
|
|
982
982
|
'<button class="pr-pager-btn" onclick="refreshLiveOutput()" style="font-size:10px">Refresh now</button>' +
|
|
983
983
|
'</div>';
|
|
984
|
-
|
|
984
|
+
startLiveStream(currentAgentId);
|
|
985
985
|
} else if (tab === 'charter') {
|
|
986
986
|
el.innerHTML = '<div class="section">' + escHtml(detail.charter || 'No charter found.') + '</div>';
|
|
987
987
|
} else if (tab === 'history') {
|
|
@@ -1011,6 +1011,49 @@ function renderDetailContent(detail, tab) {
|
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
1013
|
let livePollingInterval = null;
|
|
1014
|
+
let liveEventSource = null;
|
|
1015
|
+
|
|
1016
|
+
function startLiveStream(agentId) {
|
|
1017
|
+
stopLiveStream();
|
|
1018
|
+
if (!agentId) return;
|
|
1019
|
+
|
|
1020
|
+
const outputEl = document.getElementById('live-output');
|
|
1021
|
+
if (outputEl) outputEl.textContent = '';
|
|
1022
|
+
|
|
1023
|
+
liveEventSource = new EventSource('/api/agent/' + agentId + '/live-stream');
|
|
1024
|
+
|
|
1025
|
+
liveEventSource.onmessage = function(e) {
|
|
1026
|
+
try {
|
|
1027
|
+
const chunk = JSON.parse(e.data);
|
|
1028
|
+
const el = document.getElementById('live-output');
|
|
1029
|
+
if (el) {
|
|
1030
|
+
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
|
|
1031
|
+
el.textContent += chunk;
|
|
1032
|
+
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
1033
|
+
}
|
|
1034
|
+
} catch {}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
liveEventSource.addEventListener('done', function() {
|
|
1038
|
+
stopLiveStream();
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
liveEventSource.onerror = function() {
|
|
1042
|
+
// Fall back to polling on SSE error
|
|
1043
|
+
stopLiveStream();
|
|
1044
|
+
startLivePolling();
|
|
1045
|
+
const label = document.getElementById('live-status-label');
|
|
1046
|
+
if (label) label.textContent = 'Auto-refreshing every 3s';
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function stopLiveStream() {
|
|
1051
|
+
if (liveEventSource) {
|
|
1052
|
+
liveEventSource.close();
|
|
1053
|
+
liveEventSource = null;
|
|
1054
|
+
}
|
|
1055
|
+
stopLivePolling();
|
|
1056
|
+
}
|
|
1014
1057
|
|
|
1015
1058
|
function startLivePolling() {
|
|
1016
1059
|
stopLivePolling();
|
|
@@ -2209,6 +2252,7 @@ function renderDispatch(dispatch) {
|
|
|
2209
2252
|
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
2210
2253
|
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
2211
2254
|
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
2255
|
+
(d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
2212
2256
|
'</div>'
|
|
2213
2257
|
).join('') + '</div>';
|
|
2214
2258
|
} else {
|
|
@@ -2516,6 +2560,7 @@ function wiRow(item) {
|
|
|
2516
2560
|
'<td>' + typeBadge(item.type) + '</td>' +
|
|
2517
2561
|
'<td>' + priBadge(item.priority) + '</td>' +
|
|
2518
2562
|
'<td>' + statusBadge(item.status || 'pending') +
|
|
2563
|
+
(item._pendingReason ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' + escHtml(item._pendingReason) + '">' + escHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
2519
2564
|
(item.status === 'failed' ? ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry</button>' : '') +
|
|
2520
2565
|
'</td>' +
|
|
2521
2566
|
'<td>' +
|
|
@@ -2527,6 +2572,7 @@ function wiRow(item) {
|
|
|
2527
2572
|
'<td>' + prLink + '</td>' +
|
|
2528
2573
|
'<td><span class="pr-date">' + shortTime(item.created) + '</span></td>' +
|
|
2529
2574
|
'<td style="white-space:nowrap">' +
|
|
2575
|
+
((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Edit work item">✎</button>' : '') +
|
|
2530
2576
|
((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Archive work item">📦</button>' : '') +
|
|
2531
2577
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
2532
2578
|
'</td>' +
|
|
@@ -2588,6 +2634,63 @@ async function retryWorkItem(id, source) {
|
|
|
2588
2634
|
} catch (e) { alert('Retry error: ' + e.message); }
|
|
2589
2635
|
}
|
|
2590
2636
|
|
|
2637
|
+
function editWorkItem(id, source) {
|
|
2638
|
+
const item = allWorkItems.find(i => i.id === id);
|
|
2639
|
+
if (!item) return;
|
|
2640
|
+
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'investigate', 'refactor', 'test', 'docs'];
|
|
2641
|
+
const priorities = ['critical', 'high', 'medium', 'low'];
|
|
2642
|
+
const agentOpts = cmdAgents.map(a => '<option value="' + escHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
2643
|
+
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
2644
|
+
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((item.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
2645
|
+
|
|
2646
|
+
document.getElementById('modal-title').textContent = 'Edit Work Item ' + id;
|
|
2647
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
2648
|
+
document.getElementById('modal-body').innerHTML =
|
|
2649
|
+
'<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
2650
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Title' +
|
|
2651
|
+
'<input id="wi-edit-title" value="' + escHtml(item.title || '') + '" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit">' +
|
|
2652
|
+
'</label>' +
|
|
2653
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Description' +
|
|
2654
|
+
'<textarea id="wi-edit-desc" rows="3" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit;resize:vertical">' + escHtml(item.description || '') + '</textarea>' +
|
|
2655
|
+
'</label>' +
|
|
2656
|
+
'<div style="display:flex;gap:12px">' +
|
|
2657
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Type' +
|
|
2658
|
+
'<select id="wi-edit-type" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' + typeOpts + '</select>' +
|
|
2659
|
+
'</label>' +
|
|
2660
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Priority' +
|
|
2661
|
+
'<select id="wi-edit-priority" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)">' + priOpts + '</select>' +
|
|
2662
|
+
'</label>' +
|
|
2663
|
+
'<label style="color:var(--text);font-size:var(--text-md);flex:1">Agent' +
|
|
2664
|
+
'<select id="wi-edit-agent" style="display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)"><option value="">Auto</option>' + agentOpts + '</select>' +
|
|
2665
|
+
'</label>' +
|
|
2666
|
+
'</div>' +
|
|
2667
|
+
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
|
|
2668
|
+
'<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
|
|
2669
|
+
'<button onclick="submitWorkItemEdit(\'' + escHtml(id) + '\',\'' + escHtml(source || '') + '\')" style="padding:6px 16px;font-size:var(--text-md);background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Save</button>' +
|
|
2670
|
+
'</div>' +
|
|
2671
|
+
'</div>';
|
|
2672
|
+
document.getElementById('modal').classList.add('open');
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
async function submitWorkItemEdit(id, source) {
|
|
2676
|
+
const title = document.getElementById('wi-edit-title').value.trim();
|
|
2677
|
+
const description = document.getElementById('wi-edit-desc').value;
|
|
2678
|
+
const type = document.getElementById('wi-edit-type').value;
|
|
2679
|
+
const priority = document.getElementById('wi-edit-priority').value;
|
|
2680
|
+
const agent = document.getElementById('wi-edit-agent').value;
|
|
2681
|
+
if (!title) { alert('Title is required'); return; }
|
|
2682
|
+
try {
|
|
2683
|
+
const res = await fetch('/api/work-items/update', {
|
|
2684
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2685
|
+
body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent })
|
|
2686
|
+
});
|
|
2687
|
+
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Work item updated', true); } else {
|
|
2688
|
+
const d = await res.json();
|
|
2689
|
+
alert('Update failed: ' + (d.error || 'unknown'));
|
|
2690
|
+
}
|
|
2691
|
+
} catch (e) { alert('Update error: ' + e.message); }
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2591
2694
|
async function deleteWorkItem(id, source) {
|
|
2592
2695
|
if (!confirm('Delete work item ' + id + '? This will kill any running agent and remove all dispatch history.')) return;
|
|
2593
2696
|
try {
|
package/dashboard.js
CHANGED
|
@@ -852,6 +852,44 @@ const server = http.createServer(async (req, res) => {
|
|
|
852
852
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
+
// POST /api/work-items/update — edit a pending/failed work item
|
|
856
|
+
if (req.method === 'POST' && req.url === '/api/work-items/update') {
|
|
857
|
+
try {
|
|
858
|
+
const body = await readBody(req);
|
|
859
|
+
const { id, source, title, description, type, priority, agent } = body;
|
|
860
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
861
|
+
|
|
862
|
+
let wiPath;
|
|
863
|
+
if (!source || source === 'central') {
|
|
864
|
+
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
865
|
+
} else {
|
|
866
|
+
const proj = PROJECTS.find(p => p.name === source);
|
|
867
|
+
if (proj) {
|
|
868
|
+
wiPath = shared.projectWorkItemsPath(proj);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
872
|
+
|
|
873
|
+
const items = JSON.parse(safeRead(wiPath) || '[]');
|
|
874
|
+
const item = items.find(i => i.id === id);
|
|
875
|
+
if (!item) return jsonReply(res, 404, { error: 'item not found' });
|
|
876
|
+
|
|
877
|
+
if (item.status === 'dispatched') {
|
|
878
|
+
return jsonReply(res, 400, { error: 'Cannot edit dispatched items' });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (title !== undefined) item.title = title;
|
|
882
|
+
if (description !== undefined) item.description = description;
|
|
883
|
+
if (type !== undefined) item.type = type;
|
|
884
|
+
if (priority !== undefined) item.priority = priority;
|
|
885
|
+
if (agent !== undefined) item.agent = agent || null;
|
|
886
|
+
item.updatedAt = new Date().toISOString();
|
|
887
|
+
|
|
888
|
+
safeWrite(wiPath, items);
|
|
889
|
+
return jsonReply(res, 200, { ok: true, item });
|
|
890
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
891
|
+
}
|
|
892
|
+
|
|
855
893
|
// POST /api/notes — write to inbox so it flows through normal consolidation
|
|
856
894
|
if (req.method === 'POST' && req.url === '/api/notes') {
|
|
857
895
|
try {
|
|
@@ -1076,6 +1114,69 @@ const server = http.createServer(async (req, res) => {
|
|
|
1076
1114
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1077
1115
|
}
|
|
1078
1116
|
|
|
1117
|
+
// GET /api/agent/:id/live-stream — SSE real-time live output streaming
|
|
1118
|
+
const liveStreamMatch = req.url.match(/^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/);
|
|
1119
|
+
if (liveStreamMatch && req.method === 'GET') {
|
|
1120
|
+
const agentId = liveStreamMatch[1];
|
|
1121
|
+
const liveLogPath = path.join(MINIONS_DIR, 'agents', agentId, 'live-output.log');
|
|
1122
|
+
|
|
1123
|
+
res.writeHead(200, {
|
|
1124
|
+
'Content-Type': 'text/event-stream',
|
|
1125
|
+
'Cache-Control': 'no-cache',
|
|
1126
|
+
'Connection': 'keep-alive',
|
|
1127
|
+
'Access-Control-Allow-Origin': '*',
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// Send initial content
|
|
1131
|
+
let offset = 0;
|
|
1132
|
+
try {
|
|
1133
|
+
const content = fs.readFileSync(liveLogPath, 'utf8');
|
|
1134
|
+
if (content.length > 0) {
|
|
1135
|
+
res.write(`data: ${JSON.stringify(content)}\n\n`);
|
|
1136
|
+
offset = Buffer.byteLength(content, 'utf8');
|
|
1137
|
+
}
|
|
1138
|
+
} catch {}
|
|
1139
|
+
|
|
1140
|
+
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1141
|
+
const watcher = () => {
|
|
1142
|
+
try {
|
|
1143
|
+
const stat = fs.statSync(liveLogPath);
|
|
1144
|
+
if (stat.size > offset) {
|
|
1145
|
+
const fd = fs.openSync(liveLogPath, 'r');
|
|
1146
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
1147
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
1148
|
+
fs.closeSync(fd);
|
|
1149
|
+
offset = stat.size;
|
|
1150
|
+
const chunk = buf.toString('utf8');
|
|
1151
|
+
if (chunk) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
1152
|
+
}
|
|
1153
|
+
} catch {}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
fs.watchFile(liveLogPath, { interval: 500 }, watcher);
|
|
1157
|
+
|
|
1158
|
+
// Check if agent is still active (poll every 5s)
|
|
1159
|
+
const doneCheck = setInterval(() => {
|
|
1160
|
+
const dispatch = getDispatchQueue();
|
|
1161
|
+
const isActive = (dispatch.active || []).some(d => d.agent === agentId);
|
|
1162
|
+
if (!isActive) {
|
|
1163
|
+
watcher(); // flush final content
|
|
1164
|
+
res.write(`event: done\ndata: complete\n\n`);
|
|
1165
|
+
clearInterval(doneCheck);
|
|
1166
|
+
fs.unwatchFile(liveLogPath, watcher);
|
|
1167
|
+
res.end();
|
|
1168
|
+
}
|
|
1169
|
+
}, 5000);
|
|
1170
|
+
|
|
1171
|
+
// Cleanup on client disconnect
|
|
1172
|
+
req.on('close', () => {
|
|
1173
|
+
clearInterval(doneCheck);
|
|
1174
|
+
fs.unwatchFile(liveLogPath, watcher);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1079
1180
|
// GET /api/agent/:id/live — tail live output for a working agent
|
|
1080
1181
|
const liveMatch = req.url.match(/^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/);
|
|
1081
1182
|
if (liveMatch && req.method === 'GET') {
|
package/docs/auto-discovery.md
CHANGED
|
@@ -196,7 +196,7 @@ Discovered items land in `engine/dispatch.json`:
|
|
|
196
196
|
Each tick, the engine checks available slots:
|
|
197
197
|
|
|
198
198
|
```
|
|
199
|
-
slotsAvailable = maxConcurrent (
|
|
199
|
+
slotsAvailable = maxConcurrent (5) - activeCount
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
It takes up to `slotsAvailable` items from pending and spawns them. Items are processed in discovery-priority order (fixes first, then reviews, then implements, then work-items).
|
|
@@ -378,7 +378,7 @@ All discovery behavior is controlled via `config.json`:
|
|
|
378
378
|
{
|
|
379
379
|
"engine": {
|
|
380
380
|
"tickInterval": 60000, // ms between ticks
|
|
381
|
-
"maxConcurrent":
|
|
381
|
+
"maxConcurrent": 5, // max agents running at once
|
|
382
382
|
"agentTimeout": 18000000, // 5 hours — kill hung processes
|
|
383
383
|
"heartbeatTimeout": 300000, // 5min — kill stale/silent agents
|
|
384
384
|
"maxTurns": 100, // max claude CLI turns per agent
|
package/engine/ado.js
CHANGED
|
@@ -192,6 +192,7 @@ async function pollPrStatus(config) {
|
|
|
192
192
|
pr.buildStatus = buildStatus;
|
|
193
193
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
194
194
|
else delete pr.buildFailReason;
|
|
195
|
+
if (buildStatus !== 'failing') delete pr._buildFailNotified;
|
|
195
196
|
updated = true;
|
|
196
197
|
}
|
|
197
198
|
|