@yemi33/squad 0.1.16 → 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 +88 -21
- 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
|
@@ -797,8 +797,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
// Post-completion: scan output for PRs and sync to pull-requests.json
|
|
800
|
+
let prsCreatedCount = 0;
|
|
800
801
|
if (code === 0) {
|
|
801
|
-
syncPrsFromOutput(stdout, agentId, meta, config);
|
|
802
|
+
prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
|
|
802
803
|
}
|
|
803
804
|
|
|
804
805
|
// Post-completion: update PR status if relevant
|
|
@@ -817,11 +818,12 @@ function spawnAgent(dispatchItem, config) {
|
|
|
817
818
|
updateAgentHistory(agentId, dispatchItem, code === 0 ? 'success' : 'error');
|
|
818
819
|
|
|
819
820
|
// Update quality metrics
|
|
820
|
-
updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error', taskUsage);
|
|
821
|
+
updateMetrics(agentId, dispatchItem, code === 0 ? 'success' : 'error', taskUsage, prsCreatedCount);
|
|
821
822
|
|
|
822
|
-
// Cleanup temp files
|
|
823
|
+
// Cleanup temp files (including PID file now that dispatch is complete)
|
|
823
824
|
try { fs.unlinkSync(sysPromptPath); } catch {}
|
|
824
825
|
try { fs.unlinkSync(promptPath); } catch {}
|
|
826
|
+
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
|
|
825
827
|
|
|
826
828
|
log('info', `Agent ${agentId} completed. Output saved to ${outputPath}`);
|
|
827
829
|
});
|
|
@@ -856,6 +858,7 @@ function spawnAgent(dispatchItem, config) {
|
|
|
856
858
|
}
|
|
857
859
|
|
|
858
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
|
|
859
862
|
setTimeout(() => {
|
|
860
863
|
const pidFile = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
861
864
|
try {
|
|
@@ -863,12 +866,10 @@ function spawnAgent(dispatchItem, config) {
|
|
|
863
866
|
if (pidStr) {
|
|
864
867
|
log('info', `Agent ${agentId} verified via PID file: ${pidStr}`);
|
|
865
868
|
}
|
|
866
|
-
|
|
869
|
+
// Don't delete — keep for re-attachment on engine restart
|
|
867
870
|
} catch {
|
|
868
|
-
// No PID file — check if live output exists (spawn-agent.js may have written it)
|
|
869
871
|
if (!fs.existsSync(liveOutputPath) || fs.statSync(liveOutputPath).size <= 200) {
|
|
870
872
|
log('error', `Agent ${agentId} (${id}) — no PID file and no output after 5s. Spawn likely failed.`);
|
|
871
|
-
// Don't mark as error yet — heartbeat will catch it at 5min
|
|
872
873
|
}
|
|
873
874
|
}
|
|
874
875
|
}, 5000);
|
|
@@ -1045,10 +1046,7 @@ ${itemSummary}
|
|
|
1045
1046
|
// a plan-to-prd task so Lambert converts it to structured PRD items.
|
|
1046
1047
|
function chainPlanToPrd(dispatchItem, meta, config) {
|
|
1047
1048
|
const planDir = path.join(SQUAD_DIR, 'plans');
|
|
1048
|
-
if (!fs.existsSync(planDir)) {
|
|
1049
|
-
log('warn', `Plan chaining: no plans/ directory found after plan task ${dispatchItem.id}`);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1049
|
+
if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
|
|
1052
1050
|
|
|
1053
1051
|
// Use the plan filename from dispatch meta (set during plan task creation)
|
|
1054
1052
|
// Falls back to mtime-based detection if meta doesn't have it
|
|
@@ -1322,7 +1320,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
1322
1320
|
while ((match = prHeaderPattern.exec(content)) !== null) prMatches.add(match[1] || match[2]);
|
|
1323
1321
|
}
|
|
1324
1322
|
|
|
1325
|
-
if (prMatches.size === 0) return;
|
|
1323
|
+
if (prMatches.size === 0) return 0;
|
|
1326
1324
|
|
|
1327
1325
|
// Determine which project to add PRs to
|
|
1328
1326
|
const projects = getProjects(config);
|
|
@@ -1385,6 +1383,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
1385
1383
|
safeWrite(prPath, prs);
|
|
1386
1384
|
log('info', `Synced ${added} PR(s) from ${agentName}'s output to ${targetProject.name}/pull-requests.json`);
|
|
1387
1385
|
}
|
|
1386
|
+
return added;
|
|
1388
1387
|
}
|
|
1389
1388
|
|
|
1390
1389
|
// ─── Post-Completion Hooks ──────────────────────────────────────────────────
|
|
@@ -1411,14 +1410,23 @@ function updatePrAfterReview(agentId, pr, project) {
|
|
|
1411
1410
|
note: agentStatus.task || ''
|
|
1412
1411
|
};
|
|
1413
1412
|
|
|
1414
|
-
// Update author metrics
|
|
1413
|
+
// Update author metrics (deduplicated per PR — don't double-count re-reviews)
|
|
1415
1414
|
const authorAgentId = (pr.agent || '').toLowerCase();
|
|
1416
1415
|
if (authorAgentId && config.agents?.[authorAgentId]) {
|
|
1417
1416
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
1418
1417
|
const metrics = safeJson(metricsPath) || {};
|
|
1419
1418
|
if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
|
|
1420
|
-
if (
|
|
1421
|
-
|
|
1419
|
+
if (!metrics[authorAgentId]._reviewedPrs) metrics[authorAgentId]._reviewedPrs = {};
|
|
1420
|
+
const prevVerdict = metrics[authorAgentId]._reviewedPrs[pr.id];
|
|
1421
|
+
if (prevVerdict !== squadVerdict) {
|
|
1422
|
+
// Undo previous count if verdict changed (e.g. approved → changes-requested)
|
|
1423
|
+
if (prevVerdict === 'approved') metrics[authorAgentId].prsApproved = Math.max(0, (metrics[authorAgentId].prsApproved || 0) - 1);
|
|
1424
|
+
else if (prevVerdict === 'changes-requested') metrics[authorAgentId].prsRejected = Math.max(0, (metrics[authorAgentId].prsRejected || 0) - 1);
|
|
1425
|
+
// Apply new verdict
|
|
1426
|
+
if (squadVerdict === 'approved') metrics[authorAgentId].prsApproved++;
|
|
1427
|
+
else if (squadVerdict === 'changes-requested') metrics[authorAgentId].prsRejected++;
|
|
1428
|
+
metrics[authorAgentId]._reviewedPrs[pr.id] = squadVerdict;
|
|
1429
|
+
}
|
|
1422
1430
|
safeWrite(metricsPath, metrics);
|
|
1423
1431
|
}
|
|
1424
1432
|
|
|
@@ -2002,7 +2010,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
|
|
|
2002
2010
|
log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
2003
2011
|
}
|
|
2004
2012
|
|
|
2005
|
-
function updateMetrics(agentId, dispatchItem, result, taskUsage) {
|
|
2013
|
+
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount) {
|
|
2006
2014
|
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
2007
2015
|
const metrics = safeJson(metricsPath) || {};
|
|
2008
2016
|
|
|
@@ -2029,7 +2037,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage) {
|
|
|
2029
2037
|
|
|
2030
2038
|
if (result === 'success') {
|
|
2031
2039
|
m.tasksCompleted++;
|
|
2032
|
-
if (
|
|
2040
|
+
if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
|
|
2033
2041
|
if (dispatchItem.type === 'review') m.reviewsDone++;
|
|
2034
2042
|
} else {
|
|
2035
2043
|
m.tasksErrored++;
|
|
@@ -3848,6 +3856,8 @@ function discoverCentralWorkItems(config) {
|
|
|
3848
3856
|
|
|
3849
3857
|
// Inject plan-specific variables for the plan playbook
|
|
3850
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 });
|
|
3851
3861
|
const planFileName = `plan-${item.id.toLowerCase()}-${dateStamp()}.md`;
|
|
3852
3862
|
vars.plan_content = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3853
3863
|
vars.plan_title = item.title;
|
|
@@ -4080,15 +4090,72 @@ const commands = {
|
|
|
4080
4090
|
// Load persistent state
|
|
4081
4091
|
loadCooldowns();
|
|
4082
4092
|
|
|
4083
|
-
//
|
|
4093
|
+
// Re-attach to surviving agent processes from previous session
|
|
4084
4094
|
const dispatch = getDispatch();
|
|
4085
4095
|
const activeOnStart = (dispatch.active || []);
|
|
4086
4096
|
if (activeOnStart.length > 0) {
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
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
|
+
}
|
|
4090
4156
|
for (const item of activeOnStart) {
|
|
4091
|
-
|
|
4157
|
+
const attached = activeProcesses.has(item.id);
|
|
4158
|
+
console.log(` ${attached ? '✓' : '?'} ${item.agentName || item.agent}: ${(item.task || '').slice(0, 70)}`);
|
|
4092
4159
|
}
|
|
4093
4160
|
}
|
|
4094
4161
|
|
package/package.json
CHANGED