@yemi33/squad 0.1.17 → 0.1.19

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 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: 240px; 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; }
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</h2>
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
- if (!notes || !notes.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
942
- el.innerHTML = '<div class="notes-preview" onclick="openNotesModal()" title="Click to expand">' + escHtml(notes) + '</div>';
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 = preview.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
- return safeRead(path.join(SQUAD_DIR, 'notes.md')) || '';
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
- try { fs.unlinkSync(pidFile); } catch {}
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
- // Grace period for agents that survived a restart
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
- const gracePeriod = config.engine?.restartGracePeriod || 1200000; // 20min default
4099
- engineRestartGraceUntil = Date.now() + gracePeriod;
4100
- console.log(` ${activeOnStart.length} active dispatch(es) from previous session ${gracePeriod / 60000}min grace period before orphan detection`);
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
- console.log(` - ${item.agentName || item.agent}: ${(item.task || '').slice(0, 70)}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.squad/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "squad": "bin/squad.js"