@yemi33/squad 0.1.15 → 0.1.17

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.
Files changed (3) hide show
  1. package/dashboard.js +48 -0
  2. package/engine.js +19 -8
  3. package/package.json +1 -1
package/dashboard.js CHANGED
@@ -1394,6 +1394,54 @@ server.listen(PORT, '127.0.0.1', () => {
1394
1394
  console.log(` Could not auto-open browser: ${e.message}`);
1395
1395
  console.log(` Please open http://localhost:${PORT} manually.`);
1396
1396
  }
1397
+
1398
+ // ─── Engine Watchdog ─────────────────────────────────────────────────────
1399
+ // Every 30s, check if engine PID is alive. If dead but control.json says
1400
+ // running, auto-restart it. Prevents silent engine death.
1401
+ const { execSync, spawn: cpSpawn } = require('child_process');
1402
+ setInterval(() => {
1403
+ try {
1404
+ const control = getEngineState();
1405
+ if (control.state !== 'running' || !control.pid) return;
1406
+
1407
+ // Check if PID is alive
1408
+ let alive = false;
1409
+ try {
1410
+ if (process.platform === 'win32') {
1411
+ const out = execSync(`tasklist /FI "PID eq ${control.pid}" /NH`, { encoding: 'utf8', timeout: 3000 });
1412
+ alive = out.includes(String(control.pid));
1413
+ } else {
1414
+ process.kill(control.pid, 0); // signal 0 = check existence
1415
+ alive = true;
1416
+ }
1417
+ } catch { alive = false; }
1418
+
1419
+ if (!alive) {
1420
+ console.log(`[watchdog] Engine PID ${control.pid} is dead — auto-restarting...`);
1421
+
1422
+ // Set state to stopped first
1423
+ const controlPath = path.join(SQUAD_DIR, 'engine', 'control.json');
1424
+ safeWrite(controlPath, { state: 'stopped', pid: null, crashed_at: new Date().toISOString() });
1425
+
1426
+ // Restart engine
1427
+ const childEnv = { ...process.env };
1428
+ for (const key of Object.keys(childEnv)) {
1429
+ if (key === 'CLAUDECODE' || key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete childEnv[key];
1430
+ }
1431
+ const engineProc = cpSpawn(process.execPath, [path.join(SQUAD_DIR, 'engine.js'), 'start'], {
1432
+ cwd: SQUAD_DIR,
1433
+ stdio: 'ignore',
1434
+ detached: true,
1435
+ env: childEnv,
1436
+ });
1437
+ engineProc.unref();
1438
+ console.log(`[watchdog] Engine restarted (new PID: ${engineProc.pid})`);
1439
+ }
1440
+ } catch (e) {
1441
+ console.error(`[watchdog] Error: ${e.message}`);
1442
+ }
1443
+ }, 30000);
1444
+ console.log(` Engine watchdog: active (checks every 30s)`);
1397
1445
  });
1398
1446
 
1399
1447
  server.on('error', e => {
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,7 +818,7 @@ 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
823
  // Cleanup temp files
823
824
  try { fs.unlinkSync(sysPromptPath); } catch {}
@@ -1322,7 +1323,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
1322
1323
  while ((match = prHeaderPattern.exec(content)) !== null) prMatches.add(match[1] || match[2]);
1323
1324
  }
1324
1325
 
1325
- if (prMatches.size === 0) return;
1326
+ if (prMatches.size === 0) return 0;
1326
1327
 
1327
1328
  // Determine which project to add PRs to
1328
1329
  const projects = getProjects(config);
@@ -1385,6 +1386,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
1385
1386
  safeWrite(prPath, prs);
1386
1387
  log('info', `Synced ${added} PR(s) from ${agentName}'s output to ${targetProject.name}/pull-requests.json`);
1387
1388
  }
1389
+ return added;
1388
1390
  }
1389
1391
 
1390
1392
  // ─── Post-Completion Hooks ──────────────────────────────────────────────────
@@ -1411,14 +1413,23 @@ function updatePrAfterReview(agentId, pr, project) {
1411
1413
  note: agentStatus.task || ''
1412
1414
  };
1413
1415
 
1414
- // Update author metrics
1416
+ // Update author metrics (deduplicated per PR — don't double-count re-reviews)
1415
1417
  const authorAgentId = (pr.agent || '').toLowerCase();
1416
1418
  if (authorAgentId && config.agents?.[authorAgentId]) {
1417
1419
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
1418
1420
  const metrics = safeJson(metricsPath) || {};
1419
1421
  if (!metrics[authorAgentId]) metrics[authorAgentId] = { tasksCompleted:0, tasksErrored:0, prsCreated:0, prsApproved:0, prsRejected:0, reviewsDone:0, lastTask:null, lastCompleted:null };
1420
- if (squadVerdict === 'approved') metrics[authorAgentId].prsApproved++;
1421
- else if (squadVerdict === 'changes-requested') metrics[authorAgentId].prsRejected++;
1422
+ if (!metrics[authorAgentId]._reviewedPrs) metrics[authorAgentId]._reviewedPrs = {};
1423
+ const prevVerdict = metrics[authorAgentId]._reviewedPrs[pr.id];
1424
+ if (prevVerdict !== squadVerdict) {
1425
+ // Undo previous count if verdict changed (e.g. approved → changes-requested)
1426
+ if (prevVerdict === 'approved') metrics[authorAgentId].prsApproved = Math.max(0, (metrics[authorAgentId].prsApproved || 0) - 1);
1427
+ else if (prevVerdict === 'changes-requested') metrics[authorAgentId].prsRejected = Math.max(0, (metrics[authorAgentId].prsRejected || 0) - 1);
1428
+ // Apply new verdict
1429
+ if (squadVerdict === 'approved') metrics[authorAgentId].prsApproved++;
1430
+ else if (squadVerdict === 'changes-requested') metrics[authorAgentId].prsRejected++;
1431
+ metrics[authorAgentId]._reviewedPrs[pr.id] = squadVerdict;
1432
+ }
1422
1433
  safeWrite(metricsPath, metrics);
1423
1434
  }
1424
1435
 
@@ -2002,7 +2013,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config) {
2002
2013
  log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
2003
2014
  }
2004
2015
 
2005
- function updateMetrics(agentId, dispatchItem, result, taskUsage) {
2016
+ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount) {
2006
2017
  const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
2007
2018
  const metrics = safeJson(metricsPath) || {};
2008
2019
 
@@ -2029,7 +2040,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage) {
2029
2040
 
2030
2041
  if (result === 'success') {
2031
2042
  m.tasksCompleted++;
2032
- if (dispatchItem.type === 'implement') m.prsCreated++;
2043
+ if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
2033
2044
  if (dispatchItem.type === 'review') m.reviewsDone++;
2034
2045
  } else {
2035
2046
  m.tasksErrored++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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"