@yemi33/squad 0.1.17 → 0.1.18
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.html +84 -5
- package/dashboard.js +19 -1
- package/docs/human-vs-automated.md +107 -0
- package/engine.js +69 -13
- package/package.json +1 -1
package/dashboard.html
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
.prd-item-priority.low { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
152
152
|
.prd-project-badge { font-size: 9px; padding: 1px 5px; border-radius: 6px; background: rgba(56,139,253,0.12); color: var(--blue); border: 1px solid rgba(56,139,253,0.25); white-space: nowrap; }
|
|
153
153
|
|
|
154
|
-
.notes-preview { max-height:
|
|
154
|
+
.notes-preview { max-height: 400px; overflow-y: auto; font-size: 12px; line-height: 1.6; color: var(--muted); font-family: Consolas, monospace; white-space: pre-wrap; word-wrap: break-word; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; cursor: pointer; transition: border-color 0.2s; }
|
|
155
155
|
.notes-preview:hover { border-color: var(--blue); }
|
|
156
156
|
.inbox-item { background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--purple); border-radius: 4px; padding: 10px 12px; cursor: pointer; }
|
|
157
157
|
.inbox-item:hover { border-color: var(--blue); border-left-color: var(--blue); }
|
|
@@ -553,7 +553,7 @@
|
|
|
553
553
|
</section>
|
|
554
554
|
|
|
555
555
|
<section>
|
|
556
|
-
<h2>Team Notes
|
|
556
|
+
<h2>Team Notes <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0" id="notes-updated"></span></h2>
|
|
557
557
|
<div id="notes-list">Loading...</div>
|
|
558
558
|
</section>
|
|
559
559
|
|
|
@@ -621,6 +621,9 @@
|
|
|
621
621
|
<div class="modal-header">
|
|
622
622
|
<h3 id="modal-title">—</h3>
|
|
623
623
|
<div class="modal-header-actions">
|
|
624
|
+
<button class="modal-copy" id="modal-edit-btn" onclick="modalToggleEdit()" title="Edit" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zM12.9 2.97L4.288 11.58l-.537 1.878 1.878-.537L14.242 4.31 12.9 2.97z"/></svg> Edit</button>
|
|
625
|
+
<button class="modal-copy" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none;color:var(--green);border-color:var(--green)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
|
|
626
|
+
<button class="modal-copy" id="modal-cancel-edit-btn" onclick="modalCancelEdit()" title="Cancel edit" style="display:none">Cancel</button>
|
|
624
627
|
<button class="modal-copy" id="modal-copy-btn" onclick="copyModalContent()" title="Copy to clipboard"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/></svg> Copy</button>
|
|
625
628
|
<button class="modal-close" onclick="closeModal()">X</button>
|
|
626
629
|
</div>
|
|
@@ -938,20 +941,87 @@ async function openInboxInExplorer(name) {
|
|
|
938
941
|
|
|
939
942
|
function renderNotes(notes) {
|
|
940
943
|
const el = document.getElementById('notes-list');
|
|
941
|
-
|
|
942
|
-
|
|
944
|
+
const content = typeof notes === 'object' ? notes.content : notes;
|
|
945
|
+
const updatedAt = typeof notes === 'object' ? notes.updatedAt : null;
|
|
946
|
+
|
|
947
|
+
// Show last updated timestamp
|
|
948
|
+
const updatedEl = document.getElementById('notes-updated');
|
|
949
|
+
if (updatedEl && updatedAt) {
|
|
950
|
+
const d = new Date(updatedAt);
|
|
951
|
+
updatedEl.textContent = 'updated ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
955
|
+
el.innerHTML = '<div class="notes-preview" onclick="openNotesModal()" title="Click to expand">' + escHtml(content) + '</div>';
|
|
943
956
|
}
|
|
944
957
|
|
|
958
|
+
let _modalEditable = null; // tracks which file is editable (e.g., 'notes.md')
|
|
959
|
+
|
|
945
960
|
function openNotesModal() {
|
|
946
961
|
const preview = document.querySelector('.notes-preview');
|
|
947
962
|
if (!preview) return;
|
|
963
|
+
const content = preview.textContent;
|
|
948
964
|
document.getElementById('modal-title').textContent = 'Team Notes';
|
|
949
|
-
document.getElementById('modal-body').textContent =
|
|
965
|
+
document.getElementById('modal-body').textContent = content;
|
|
950
966
|
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
951
967
|
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
968
|
+
_modalDocContext = { title: 'Team Notes', content, selection: '' };
|
|
969
|
+
_modalEditable = 'notes.md';
|
|
970
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
952
971
|
document.getElementById('modal').classList.add('open');
|
|
953
972
|
}
|
|
954
973
|
|
|
974
|
+
function modalToggleEdit() {
|
|
975
|
+
const body = document.getElementById('modal-body');
|
|
976
|
+
body.contentEditable = 'true';
|
|
977
|
+
body.style.border = '1px solid var(--blue)';
|
|
978
|
+
body.style.borderRadius = '4px';
|
|
979
|
+
body.style.padding = '12px';
|
|
980
|
+
body.style.outline = 'none';
|
|
981
|
+
body.focus();
|
|
982
|
+
document.getElementById('modal-edit-btn').style.display = 'none';
|
|
983
|
+
document.getElementById('modal-save-btn').style.display = '';
|
|
984
|
+
document.getElementById('modal-cancel-edit-btn').style.display = '';
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function modalSaveEdit() {
|
|
988
|
+
if (!_modalEditable) return;
|
|
989
|
+
const body = document.getElementById('modal-body');
|
|
990
|
+
const content = body.innerText;
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const res = await fetch('/api/notes-save', {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
headers: { 'Content-Type': 'application/json' },
|
|
996
|
+
body: JSON.stringify({ file: _modalEditable, content }),
|
|
997
|
+
});
|
|
998
|
+
const data = await res.json();
|
|
999
|
+
if (!res.ok) throw new Error(data.error || 'Save failed');
|
|
1000
|
+
|
|
1001
|
+
body.contentEditable = 'false';
|
|
1002
|
+
body.style.border = '';
|
|
1003
|
+
body.style.padding = '';
|
|
1004
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
1005
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
1006
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
1007
|
+
_modalDocContext.content = content;
|
|
1008
|
+
showToast('cmd-toast', 'Team Notes saved', true);
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
showToast('cmd-toast', 'Error: ' + e.message, false);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function modalCancelEdit() {
|
|
1015
|
+
const body = document.getElementById('modal-body');
|
|
1016
|
+
body.contentEditable = 'false';
|
|
1017
|
+
body.textContent = _modalDocContext.content; // revert
|
|
1018
|
+
body.style.border = '';
|
|
1019
|
+
body.style.padding = '';
|
|
1020
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
1021
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
1022
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
955
1025
|
function renderPrd(prd) {
|
|
956
1026
|
const section = document.getElementById('prd-content');
|
|
957
1027
|
const badge = document.getElementById('prd-badge');
|
|
@@ -1070,6 +1140,15 @@ function closeModal() {
|
|
|
1070
1140
|
document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
|
|
1071
1141
|
document.getElementById('modal-qa-pill').style.display = 'none';
|
|
1072
1142
|
document.getElementById('ask-selection-btn').style.display = 'none';
|
|
1143
|
+
// Clear edit state
|
|
1144
|
+
_modalEditable = null;
|
|
1145
|
+
const body = document.getElementById('modal-body');
|
|
1146
|
+
body.contentEditable = 'false';
|
|
1147
|
+
body.style.border = '';
|
|
1148
|
+
body.style.padding = '';
|
|
1149
|
+
document.getElementById('modal-edit-btn').style.display = 'none';
|
|
1150
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
1151
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
1073
1152
|
}
|
|
1074
1153
|
|
|
1075
1154
|
document.addEventListener('keydown', e => {
|
package/dashboard.js
CHANGED
|
@@ -228,7 +228,12 @@ function getInbox() {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
function getNotes() {
|
|
231
|
-
|
|
231
|
+
const notesPath = path.join(SQUAD_DIR, 'notes.md');
|
|
232
|
+
const content = safeRead(notesPath) || '';
|
|
233
|
+
try {
|
|
234
|
+
const stat = fs.statSync(notesPath);
|
|
235
|
+
return { content, updatedAt: stat.mtimeMs };
|
|
236
|
+
} catch { return { content, updatedAt: null }; }
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
function getPullRequests() {
|
|
@@ -844,6 +849,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
844
849
|
return;
|
|
845
850
|
}
|
|
846
851
|
|
|
852
|
+
// POST /api/notes-save — save edited notes.md content
|
|
853
|
+
if (req.method === 'POST' && req.url === '/api/notes-save') {
|
|
854
|
+
try {
|
|
855
|
+
const body = await readBody(req);
|
|
856
|
+
if (!body.content && body.content !== '') return jsonReply(res, 400, { error: 'content required' });
|
|
857
|
+
const file = body.file || 'notes.md';
|
|
858
|
+
// Only allow saving notes.md (prevent arbitrary file writes)
|
|
859
|
+
if (file !== 'notes.md') return jsonReply(res, 400, { error: 'only notes.md can be edited' });
|
|
860
|
+
safeWrite(path.join(SQUAD_DIR, file), body.content);
|
|
861
|
+
return jsonReply(res, 200, { ok: true });
|
|
862
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
863
|
+
}
|
|
864
|
+
|
|
847
865
|
// GET /api/knowledge — list all knowledge base entries grouped by category
|
|
848
866
|
if (req.method === 'GET' && req.url === '/api/knowledge') {
|
|
849
867
|
const kbDir = path.join(SQUAD_DIR, 'knowledge');
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Human vs. Automated — What Requires You, What Doesn't
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
| Feature | Who starts it | Who runs it | Who decides | Who recovers |
|
|
6
|
+
|---------|--------------|-------------|-------------|-------------|
|
|
7
|
+
| Work items | You (dashboard) | Engine + agent | — | You (retry) |
|
|
8
|
+
| Plans | You (dashboard) | Agent writes plan | You (approve/reject) | You (revise) |
|
|
9
|
+
| PRD items | You (dashboard) | Engine dispatches | — | You (retry) |
|
|
10
|
+
| PR creation | Agent (auto) | Agent | — | — |
|
|
11
|
+
| PR review | Engine (auto-dispatch) | Agent reviewer | Human (vote to merge) | Human (comments → auto-fix) |
|
|
12
|
+
| Build failures | Engine (auto-detect) | Agent (auto-fix) | — | — |
|
|
13
|
+
| Notes | You (`/note`) or agent (findings) | Engine (consolidate) | You (promote to KB) | — |
|
|
14
|
+
| Cleanup | Engine (every 5 min) | Engine | — | — |
|
|
15
|
+
| Metrics | Engine (auto-collect) | Engine | You (view) | — |
|
|
16
|
+
| Error recovery | Engine (detect) | — | You (retry/delete) | You |
|
|
17
|
+
| Project linking | You (`squad add/scan`) | — | — | — |
|
|
18
|
+
| MCP servers | You (`~/.claude.json`) | Inherited by agents | — | — |
|
|
19
|
+
|
|
20
|
+
## The Two Human Gates
|
|
21
|
+
|
|
22
|
+
Squad is designed around **two approval gates** where humans make decisions. Everything else is automated.
|
|
23
|
+
|
|
24
|
+
### Gate 1: Plan Approval
|
|
25
|
+
|
|
26
|
+
When you submit a `/plan`, an agent creates a structured plan file. The plan sits in `awaiting-approval` status until you:
|
|
27
|
+
- **Approve** → engine auto-dispatches all plan items
|
|
28
|
+
- **Reject** → plan is archived
|
|
29
|
+
- **Revise** → feedback sent back to agent, plan is reworked
|
|
30
|
+
|
|
31
|
+
This is the only point where you decide *what* gets built.
|
|
32
|
+
|
|
33
|
+
### Gate 2: PR Review
|
|
34
|
+
|
|
35
|
+
When agents create PRs, they need human review votes before merging. You can:
|
|
36
|
+
- **Approve** → PR is merge-ready
|
|
37
|
+
- **Comment** → engine detects `@squad` mentions, auto-dispatches a fix task
|
|
38
|
+
- **Request changes** → same as comment, triggers auto-fix
|
|
39
|
+
|
|
40
|
+
This is the only point where you decide if the *quality* is good enough.
|
|
41
|
+
|
|
42
|
+
## Fully Automated (Zero Human Involvement)
|
|
43
|
+
|
|
44
|
+
These run continuously without you:
|
|
45
|
+
|
|
46
|
+
- **Work discovery** — engine scans all project queues every tick (~30s)
|
|
47
|
+
- **Agent dispatch** — engine picks the right agent, builds the prompt, spawns Claude
|
|
48
|
+
- **Worktree management** — create on dispatch, pull on shared-branch, clean after merge
|
|
49
|
+
- **PR status polling** — checks ADO for build status, review votes, merge state every ~3 min
|
|
50
|
+
- **Build failure detection** — auto-files fix tasks when CI fails
|
|
51
|
+
- **Inbox consolidation** — LLM-powered dedup and categorization when inbox hits threshold
|
|
52
|
+
- **Knowledge base classification** — auto-assigns category to consolidated notes
|
|
53
|
+
- **Heartbeat monitoring** — detects hung/dead agents, marks them failed
|
|
54
|
+
- **Blocking tool detection** — extends timeout when agent is in a long-running operation
|
|
55
|
+
- **Metrics collection** — tracks tasks, errors, PRs, approvals per agent
|
|
56
|
+
- **Dispatch priority** — fixes first, then reviews, then implementations
|
|
57
|
+
- **Cooldown & backoff** — prevents re-dispatching recently failed items
|
|
58
|
+
- **Zombie cleanup** — temp files, orphaned worktrees, stale processes every 5 min
|
|
59
|
+
- **Post-merge hooks** — worktree cleanup, PRD status update, metrics update
|
|
60
|
+
|
|
61
|
+
## Human-Triggered, Then Autonomous
|
|
62
|
+
|
|
63
|
+
You kick these off, then they run without you:
|
|
64
|
+
|
|
65
|
+
- **Work items** — type in dashboard, engine dispatches, agent executes, PR created
|
|
66
|
+
- **PRD items** — `/prd` in dashboard, engine discovers and dispatches implement tasks
|
|
67
|
+
- **Fan-out** — one task dispatched to all idle agents in parallel
|
|
68
|
+
- **Retry** — click retry on failed item, engine re-dispatches fresh
|
|
69
|
+
- **Notes** — `/note` in dashboard, flows through inbox → consolidation → team knowledge
|
|
70
|
+
- **KB promotion** — click "Add to Knowledge Base", pick category, done
|
|
71
|
+
- **Project linking** — `squad scan` or `squad add`, engine discovers work on next tick
|
|
72
|
+
|
|
73
|
+
## Human-in-the-Loop
|
|
74
|
+
|
|
75
|
+
These pause and wait for your input:
|
|
76
|
+
|
|
77
|
+
- **Plan approval** — agent writes plan, waits for approve/reject/revise
|
|
78
|
+
- **Plan discussion** — interactive Claude session where you refine the plan
|
|
79
|
+
- **PR merge** — agents can't merge their own PRs, humans must vote
|
|
80
|
+
- **PR feedback cycle** — human comments → auto-fix → human re-reviews (loop until approved)
|
|
81
|
+
|
|
82
|
+
## Manual Only
|
|
83
|
+
|
|
84
|
+
These are entirely on you:
|
|
85
|
+
|
|
86
|
+
- **Project setup** — `squad init`, `squad scan`, `squad add`
|
|
87
|
+
- **Agent customization** — edit `agents/*/charter.md`, `routing.md`
|
|
88
|
+
- **Config changes** — edit `config.json` (engine settings, projects)
|
|
89
|
+
- **MCP server setup** — add servers to `~/.claude.json`
|
|
90
|
+
- **Dashboard access** — open browser to `http://localhost:7331`
|
|
91
|
+
- **Engine start/stop** — `node engine.js start/stop`
|
|
92
|
+
|
|
93
|
+
## What Happens When You Walk Away
|
|
94
|
+
|
|
95
|
+
If you start the engine and dashboard, then leave:
|
|
96
|
+
|
|
97
|
+
1. Engine ticks every 30 seconds
|
|
98
|
+
2. Discovers pending work items, PRD gaps, PR reviews needed
|
|
99
|
+
3. Dispatches agents (up to max concurrent)
|
|
100
|
+
4. Agents create worktrees, write code, create PRs
|
|
101
|
+
5. Engine monitors for completion, hung agents, build failures
|
|
102
|
+
6. Successful work → PRs appear in your ADO/GitHub queue
|
|
103
|
+
7. Failed work → marked failed, waiting for your retry
|
|
104
|
+
8. Notes consolidated into team knowledge automatically
|
|
105
|
+
9. Worktrees cleaned up after PRs merge
|
|
106
|
+
|
|
107
|
+
**What blocks:** Plans waiting for approval. PRs waiting for your review vote. Failed tasks waiting for retry. Everything else keeps moving.
|
package/engine.js
CHANGED
|
@@ -820,9 +820,10 @@ function spawnAgent(dispatchItem, config) {
|
|
|
820
820
|
// Update quality metrics
|
|
821
821
|
updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error', taskUsage, prsCreatedCount);
|
|
822
822
|
|
|
823
|
-
// Cleanup temp files
|
|
823
|
+
// Cleanup temp files (including PID file now that dispatch is complete)
|
|
824
824
|
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
825
825
|
try { fs.unlinkSync(promptPath); } catch {}
|
|
826
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
826
827
|
|
|
827
828
|
log('info', `Agent ${agentId} completed. Output saved to ${outputPath}`);
|
|
828
829
|
});
|
|
@@ -857,6 +858,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
857
858
|
}
|
|
858
859
|
|
|
859
860
|
// Verify spawn after 5 seconds via PID file written by spawn-agent.js
|
|
861
|
+
// PID file is kept (not deleted) so engine can re-attach on restart
|
|
860
862
|
setTimeout(() => {
|
|
861
863
|
const pidFile = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
862
864
|
try {
|
|
@@ -864,12 +866,10 @@ function spawnAgent(dispatchItem, config) {
|
|
|
864
866
|
if (pidStr) {
|
|
865
867
|
log('info', `Agent ${agentId} verified via PID file: ${pidStr}`);
|
|
866
868
|
}
|
|
867
|
-
|
|
869
|
+
// Don't delete — keep for re-attachment on engine restart
|
|
868
870
|
} catch {
|
|
869
|
-
// No PID file — check if live output exists (spawn-agent.js may have written it)
|
|
870
871
|
if (!fs.existsSync(liveOutputPath) || fs.statSync(liveOutputPath).size <= 200) {
|
|
871
872
|
log('error', `Agent ${agentId} (${id}) — no PID file and no output after 5s. Spawn likely failed.`);
|
|
872
|
-
// Don't mark as error yet — heartbeat will catch it at 5min
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
875
|
}, 5000);
|
|
@@ -1046,10 +1046,7 @@ ${itemSummary}
|
|
|
1046
1046
|
// a plan-to-prd task so Lambert converts it to structured PRD items.
|
|
1047
1047
|
function chainPlanToPrd(dispatchItem, meta, config) {
|
|
1048
1048
|
const planDir = path.join(SQUAD_DIR, 'plans');
|
|
1049
|
-
if (!fs.existsSync(planDir)) {
|
|
1050
|
-
log('warn', `Plan chaining: no plans/ directory found after plan task ${dispatchItem.id}`);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1049
|
+
if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
|
|
1053
1050
|
|
|
1054
1051
|
// Use the plan filename from dispatch meta (set during plan task creation)
|
|
1055
1052
|
// Falls back to mtime-based detection if meta doesn't have it
|
|
@@ -3859,6 +3856,8 @@ function discoverCentralWorkItems(config) {
|
|
|
3859
3856
|
|
|
3860
3857
|
// Inject plan-specific variables for the plan playbook
|
|
3861
3858
|
if (workType === 'plan') {
|
|
3859
|
+
// Ensure plans directory exists before agent tries to write
|
|
3860
|
+
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
3862
3861
|
const planFileName = `plan-${item.id.toLowerCase()}-${dateStamp()}.md`;
|
|
3863
3862
|
vars.plan_content = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3864
3863
|
vars.plan_title = item.title;
|
|
@@ -4091,15 +4090,72 @@ const commands = {
|
|
|
4091
4090
|
// Load persistent state
|
|
4092
4091
|
loadCooldowns();
|
|
4093
4092
|
|
|
4094
|
-
//
|
|
4093
|
+
// Re-attach to surviving agent processes from previous session
|
|
4095
4094
|
const dispatch = getDispatch();
|
|
4096
4095
|
const activeOnStart = (dispatch.active || []);
|
|
4097
4096
|
if (activeOnStart.length > 0) {
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4097
|
+
let reattached = 0;
|
|
4098
|
+
for (const item of activeOnStart) {
|
|
4099
|
+
// Try to find the agent's PID: check status.json, or scan for live-output.log activity
|
|
4100
|
+
const agentId = item.agent;
|
|
4101
|
+
let agentPid = null;
|
|
4102
|
+
|
|
4103
|
+
// Method 1: Check PID file (if it wasn't cleaned up)
|
|
4104
|
+
const pidFile = path.join(ENGINE_DIR, `pid-${item.id}.pid`);
|
|
4105
|
+
try {
|
|
4106
|
+
const pidStr = fs.readFileSync(pidFile, 'utf8').trim();
|
|
4107
|
+
if (pidStr) agentPid = parseInt(pidStr);
|
|
4108
|
+
} catch {}
|
|
4109
|
+
|
|
4110
|
+
// Method 2: Check agent status.json for dispatch_id match
|
|
4111
|
+
if (!agentPid) {
|
|
4112
|
+
const status = getAgentStatus(agentId);
|
|
4113
|
+
if (status.dispatch_id === item.id) {
|
|
4114
|
+
// Agent was working on this dispatch — check if any process is producing output
|
|
4115
|
+
const liveLog = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
4116
|
+
try {
|
|
4117
|
+
const stat = fs.statSync(liveLog);
|
|
4118
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
4119
|
+
if (ageMs < 300000) { // live-output modified in last 5 min — agent likely alive
|
|
4120
|
+
agentPid = -1; // sentinel: alive but unknown PID
|
|
4121
|
+
}
|
|
4122
|
+
} catch {}
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// Verify PID is actually alive
|
|
4127
|
+
if (agentPid && agentPid > 0) {
|
|
4128
|
+
try {
|
|
4129
|
+
if (process.platform === 'win32') {
|
|
4130
|
+
const out = execSync(`tasklist /FI "PID eq ${agentPid}" /NH`, { encoding: 'utf8', timeout: 3000 });
|
|
4131
|
+
if (!out.includes(String(agentPid))) agentPid = null;
|
|
4132
|
+
} else {
|
|
4133
|
+
process.kill(agentPid, 0);
|
|
4134
|
+
}
|
|
4135
|
+
} catch { agentPid = null; }
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
if (agentPid) {
|
|
4139
|
+
// Re-attach: add sentinel to activeProcesses so orphan detector knows it's alive
|
|
4140
|
+
activeProcesses.set(item.id, { proc: { pid: agentPid > 0 ? agentPid : null }, agentId, startedAt: item.created_at, reattached: true });
|
|
4141
|
+
reattached++;
|
|
4142
|
+
log('info', `Re-attached to ${agentId} (${item.id}) — PID ${agentPid > 0 ? agentPid : 'unknown (active output)'}`);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
// Grace period only for dispatches we couldn't re-attach to
|
|
4147
|
+
const unattached = activeOnStart.length - reattached;
|
|
4148
|
+
if (unattached > 0) {
|
|
4149
|
+
const gracePeriod = config.engine?.restartGracePeriod || 1200000; // 20min default
|
|
4150
|
+
engineRestartGraceUntil = Date.now() + gracePeriod;
|
|
4151
|
+
console.log(` ${unattached} unattached dispatch(es) — ${gracePeriod / 60000}min grace period`);
|
|
4152
|
+
}
|
|
4153
|
+
if (reattached > 0) {
|
|
4154
|
+
console.log(` Re-attached to ${reattached} surviving agent(s)`);
|
|
4155
|
+
}
|
|
4101
4156
|
for (const item of activeOnStart) {
|
|
4102
|
-
|
|
4157
|
+
const attached = activeProcesses.has(item.id);
|
|
4158
|
+
console.log(` ${attached ? '✓' : '?'} ${item.agentName || item.agent}: ${(item.task || '').slice(0, 70)}`);
|
|
4103
4159
|
}
|
|
4104
4160
|
}
|
|
4105
4161
|
|
package/package.json
CHANGED