crewswarm 0.9.4 → 1.0.0

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 (64) hide show
  1. package/.env.example +8 -1
  2. package/README.md +58 -9
  3. package/apps/dashboard/README.md +49 -0
  4. package/apps/dashboard/dist/assets/{index-D-sRshvg.css → index-C5-vlIwl.css} +1 -1
  5. package/apps/dashboard/dist/assets/index-CSooN9fi.js +2 -0
  6. package/apps/dashboard/dist/assets/index-CSooN9fi.js.br +0 -0
  7. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js +1 -0
  8. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js.br +0 -0
  9. package/apps/dashboard/dist/assets/tab-testing-tab-Ea5K-rsb.js +1 -0
  10. package/apps/dashboard/dist/index.html +85 -7
  11. package/apps/dashboard/dist/index.html.br +0 -0
  12. package/contrib/openclaw-plugin/index.ts +20 -11
  13. package/install.sh +2 -2
  14. package/lib/autoharness/index.mjs +151 -1
  15. package/lib/chat/history.mjs +1 -1
  16. package/lib/contacts/identity-linker.mjs +24 -3
  17. package/lib/contacts/index.mjs +2 -1
  18. package/lib/crew-lead/chat-handler.mjs +56 -33
  19. package/lib/crew-lead/llm-caller.mjs +71 -14
  20. package/lib/crew-lead/prompts.mjs +4 -2
  21. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  22. package/lib/crew-lead/worktree.mjs +258 -0
  23. package/lib/crew-lead/ws-router.mjs +43 -0
  24. package/lib/engines/rt-envelope.mjs +4 -1
  25. package/lib/memory/relevance-scorer.mjs +199 -0
  26. package/lib/memory/shared-adapter.mjs +85 -19
  27. package/package.json +10 -3
  28. package/scripts/dashboard.mjs +398 -28
  29. package/scripts/health-check.mjs +70 -28
  30. package/scripts/install-docker.sh +1 -1
  31. package/scripts/restart-all-from-repo.sh +25 -21
  32. package/scripts/start.mjs +81 -26
  33. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  34. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  35. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  36. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  37. package/apps/dashboard/dist/assets/index-BeVllEj_.js +0 -2
  38. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  39. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  40. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  41. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  42. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  43. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  44. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  45. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  46. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  47. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  48. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  49. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  50. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js +0 -1
  56. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +0 -1
  60. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  64. package/apps/dashboard/dist/index.html.gz +0 -0
@@ -0,0 +1 @@
1
+ import{a as t,b as s,g as e,e as a,s as n,p as i}from"./core-utils-CmOkXgzi.js";let l=()=>{},r=()=>{};function o(t={}){l=t.hideAllViews||l,r=t.setNavActive||r}let c=null,d=null,p="";function u(){l(),document.getElementById("testingView").classList.add("active"),r("navTesting"),t.activeTab="testing",s(),y(),b(),E(),async function(){try{const t=await e("/api/tests/stale");$=new Set((t.stale||[]).map(t=>t.file))}catch{}}(),e("/api/tests/progress").then(t=>{t.running&&!k&&(x(),k=setInterval(x,2e3),L())}).catch(()=>{}),c&&clearInterval(c),c=setInterval(()=>{document.getElementById("testingView").classList.contains("active")?(y(),b()):(clearInterval(c),c=null)},3e4)}function f(t){return!t||t<=0?"-":t>=6e4?(t/6e4).toFixed(1)+"m":t>=1e3?(t/1e3).toFixed(1)+"s":Math.round(t)+"ms"}function h(t){if(!t)return"-";const s=new Date(t);return s.toLocaleDateString(void 0,{month:"short",day:"numeric"})+" "+s.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}function v(t,s){const e=t+s;return 0===e?"-":(t/e*100).toFixed(0)+"%"}const g={unit:"Unit",integration:"Integration",e2e:"E2E",playwright:"Playwright","crew-cli":"crew-cli",all:"All",unknown:"Other"},m={unit:"#818cf8",integration:"#34d399",e2e:"#fbbf24",playwright:"#f472b6","crew-cli":"#10b981",all:"#60a5fa",unknown:"#94a3b8"};let $=new Set;async function y(){var t;const s=document.getElementById("testingContent");if(s)try{const n=await e("/api/tests/summary");if(!n.latest&&!n.fileCounts)return void(s.innerHTML='<div class="empty-state">No test results found. Run tests to see results here.</div>');let i="";const l=n.fileCounts||{},r=n.testCounts||{};i+='<div class="test-launch-grid">';const o=[{key:"unit",label:"Unit",files:l.unit,tests:r.unit,cmd:"test:unit",color:m.unit},{key:"integration",label:"Integration",files:l.integration,tests:r.integration,cmd:"test:integration",color:m.integration},{key:"e2e",label:"E2E",files:l.e2e,tests:r.e2e,cmd:"test:e2e",color:m.e2e},{key:"playwright",label:"Playwright",files:l.playwright,tests:r.playwright,cmd:"test:playwright",color:"#f472b6"},{key:"crew-cli",label:"crew-cli",files:l["crew-cli"],tests:r["crew-cli"],cmd:"test",color:"#10b981"}];for(const t of o){const s=t.tests?`<span class="test-launch-tests">${t.tests} tests</span>`:"",e=t.cmd?`<button class="test-launch-btn" data-action="runTests" data-arg="${t.cmd}">▶ Run</button>`:'<span class="meta" style="font-size:10px">npx playwright test</span>';i+=`\n <div class="test-launch-card" style="border-color:${t.color}30">\n <div class="test-launch-header">\n <span class="test-launch-name" style="color:${t.color}">${t.label}</span>\n ${e}\n </div>\n <div class="test-launch-counts">\n <span class="test-launch-files">${t.files||0} files</span>\n ${s}\n </div>\n </div>`}const c=Object.values(r).reduce((t,s)=>t+(s||0),0);i+=`\n <div class="test-launch-card test-launch-total" style="border-color:var(--accent)">\n <div class="test-launch-header">\n <span class="test-launch-name" style="color:var(--accent)">All</span>\n <button class="test-launch-btn" data-action="runTests" data-arg="test:all" style="background:var(--accent);color:#fff">▶ Run All</button>\n </div>\n <div class="test-launch-counts">\n <span class="test-launch-files">${l.total||0} files</span>\n ${c?`<span class="test-launch-tests">${c}+ tests</span>`:""}\n </div>\n </div>`,i+="</div>",i+='<div class="test-section-title">Latest Results by Suite</div>',i+='<div class="test-suite-grid">';for(const s of["unit","integration","e2e","playwright","crew-cli","all"]){const e=null==(t=n.suites)?void 0:t[s];if(!e||!e.total&&!e.passed&&!e.failed)continue;const l=(e.passed||0)+(e.failed||0),r=e.failed>0?"test-status-fail":"test-status-pass",o=e.failed>0?"FAIL":"PASS",c=m[s];let d="";if(e.tests&&e.tests.length>0){const t=new Map;for(const s of e.tests){const e=s.file||"unknown";t.has(e)||t.set(e,{pass:0,fail:0,skip:0});const a=t.get(e);"pass"===s.status?a.pass++:"fail"===s.status?a.fail++:"skip"===s.status&&a.skip++}d='<div class="test-file-list">';for(const[e,n]of t){const t=e.split("/").pop(),i=e.replace(/^\/.*?CrewSwarm\//,""),l=$.has(i)||$.has(e)?'<span class="test-stale-badge" title="Source changed since last run">⚠️ stale</span>':"";d+=`\n <div class="test-file-row">\n <span class="test-file-dot">${n.fail>0?"🔴":"🟢"}</span>\n <span class="test-file-name" title="${a(e)}">${a(t)}</span>\n ${l}\n <span class="test-file-counts"><span class="test-color-pass">${n.pass}p</span> <span class="${n.fail>0?"test-color-fail":""}">${n.fail}f</span></span>\n <button class="test-file-run-btn" data-action="runSingleFile" data-arg="${a(s)}" data-arg2="${a(i)}" title="Run this file only">▶</button>\n </div>`}d+="</div>"}i+=`\n <div class="test-suite-card">\n <div class="test-suite-header">\n <span class="test-suite-name" style="color:${c}">${g[s]}</span>\n <span class="test-summary-status ${r}">${o}</span>\n </div>\n <div class="test-suite-stats">\n <div><span class="test-color-pass">${e.passed||0}</span> pass</div>\n <div><span class="${e.failed>0?"test-color-fail":""}">${e.failed||0}</span> fail</div>\n <div><span class="${e.skipped>0?"test-color-skip":""}">${e.skipped||0}</span> skip</div>\n <div><strong>${e.total||0}</strong> total</div>\n </div>\n <div class="test-suite-meta">\n ${v(e.passed||0,e.failed||0)} pass rate · ${f(e.duration_ms)} · ${h(e.timestamp)}\n </div>\n <div class="test-progress-bar">\n <div class="test-progress-pass" style="width:${l>0?(e.passed||0)/l*100:0}%"></div>\n <div class="test-progress-fail" style="width:${l>0?(e.failed||0)/l*100:0}%"></div>\n </div>\n ${d}\n </div>`}i+="</div>";const d=[];for(const t of Object.values(n.suites||{}))t.failures&&d.push(...t.failures);if(d.length>0){i+=`<div class="test-section-title">Failures (${d.length})</div>`;for(const t of d){const s="fail-"+Math.random().toString(36).slice(2),e=(t.file||"").replace(/^\/.*?CrewSwarm\//,""),n=t.rerun_command||"",l=(t.error||"").split("\n").slice(0,10).join("\n"),r=`${(t.name||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,80)}/test-failed-1.png`,o=(t.file||"").includes(".spec.")||(t.file||"").includes("playwright")?`\n <div class="test-failure-screenshot" id="ss-${a(s)}">\n <img src="/api/tests/screenshot?path=${encodeURIComponent(r)}"\n class="test-screenshot-thumb"\n alt="Failure screenshot"\n onerror="this.parentElement.style.display='none'"\n onclick="this.classList.toggle('test-screenshot-expanded')"\n title="Click to expand" />\n </div>`:"";i+=`\n <div class="test-failure-card test-failure-expandable" id="${a(s)}">\n <div class="test-failure-header" data-action="toggleFailure" data-arg="${a(s)}">\n <span class="test-failure-toggle">▶</span>\n <span class="test-failure-name">${a(t.name)}</span>\n <span class="test-failure-file-inline">${a(t.file||"")}</span>\n ${t.classification&&"unknown"!==t.classification?`<span class="test-failure-class">${a(t.classification)}</span>`:""}\n </div>\n <div class="test-failure-detail" style="display:none">\n ${t.error?`<pre class="test-failure-error">${a(String(t.error).slice(0,500))}</pre>`:""}\n ${l?`<pre class="test-failure-stack">${a(l)}</pre>`:""}\n ${o}\n <div class="test-failure-actions">\n ${e?`<a class="test-failure-link" href="#" onclick="return false" title="${a(e)}">${a(e.split("/").pop())}</a>`:""}\n ${n?`<div class="test-failure-rerun">\n <code>${a(n)}</code>\n <button class="test-copy-btn" data-action="copyText" data-arg="${a(n)}" title="Copy rerun command">Copy</button>\n </div>`:""}\n </div>\n </div>\n </div>`}}const p=[];for(const[t,s]of Object.entries(n.suites||{}))s.skips&&p.push(...s.skips.map(s=>({...s,suite:t})));if(p.length>0){i+='<details class="test-skips-section">',i+=`<summary class="test-section-title" style="cursor:pointer">Skipped (${p.length}) — click to expand</summary>`,i+='<table class="test-groups-table"><thead><tr><th>Test</th><th>File</th><th>Suite</th></tr></thead><tbody>';for(const t of p.slice(0,50))i+=`<tr><td>${a(t.name)}</td><td class="meta">${a(t.file)}</td><td><span class="test-cat-badge test-cat-${a(t.suite)}">${a(t.suite)}</span></td></tr>`;p.length>50&&(i+=`<tr><td colspan="3" class="meta">...and ${p.length-50} more</td></tr>`),i+="</tbody></table></details>"}s.innerHTML=i}catch(n){s.innerHTML=`<div class="empty-state">Failed to load test results: ${a(n.message)}</div>`}}async function b(){const t=document.getElementById("testingHistory");if(t)try{const s=await e("/api/tests/history");if(!s.history||0===s.history.length)return void(t.innerHTML='<div class="meta">No run history yet.</div>');let n='<div class="test-section-title">Run History</div>';n+='\n <table class="test-history-table">\n <thead>\n <tr>\n <th>When</th>\n <th>Suite</th>\n <th>Status</th>\n <th class="num">Pass</th>\n <th class="num">Fail</th>\n <th class="num">Skip</th>\n <th class="num">Total</th>\n <th class="num">Duration</th>\n <th class="num">Rate</th>\n </tr>\n </thead>\n <tbody>';for(const t of s.history.slice(0,25)){const s=t.failed>0?"test-color-fail":"test-color-pass",e=g[t.suite]||t.suite||"?",i=m[t.suite]||m.unknown;n+=`\n <tr data-action="loadRunDetail" data-arg="${a(t.runId)}" style="cursor:pointer" class="${t.failed>0?"test-row-fail":""}">\n <td class="meta" style="white-space:nowrap">${h(t.timestamp)}</td>\n <td><span class="test-cat-badge" style="background:${i}20;color:${i}">${e}</span></td>\n <td class="${s}" style="font-weight:600">${t.failed>0?"FAIL":"PASS"} ▸</td>\n <td class="num">${t.passed}</td>\n <td class="num ${t.failed>0?"test-color-fail":""}">${t.failed}</td>\n <td class="num ${t.skipped>0?"test-color-skip":""}">${t.skipped}</td>\n <td class="num"><strong>${t.total}</strong></td>\n <td class="num">${f(t.duration_ms)}</td>\n <td class="num">${v(t.passed,t.failed)}</td>\n </tr>`}n+="</tbody></table>",n+='<div id="testingRunDetail"></div>',t.innerHTML=n}catch(s){t.innerHTML=`<div class="meta">Failed to load history: ${a(s.message)}</div>`}}async function w(t){var s,n,i;const l=document.getElementById("testingRunDetail");if(l){l.innerHTML='<div class="meta" style="padding:12px">Loading run detail...</div>';try{const r=await e("/api/tests/run-detail?runId="+encodeURIComponent(t));if(r.error)return void(l.innerHTML=`<div class="meta">${a(r.error)}</div>`);let o=`<div class="test-section-title">Run Detail: ${a(t)} <span class="meta" style="font-weight:400;font-size:11px;text-transform:none">${h(r.timestamp)}</span></div>`;o+=`<div class="test-suite-meta" style="margin-bottom:12px">${r.passed} pass, ${r.failed} fail, ${r.skipped} skip, ${r.total} total · ${f(r.duration_ms)} · ${v(r.passed,r.failed)} pass rate</div>`;if(!((null==(s=r.failures)?void 0:s.length)>0||(null==(n=r.skips)?void 0:n.length)>0)&&r.total>0&&(o+='<div class="meta" style="padding:8px 0;color:var(--text-2)">No detailed failure/skip data saved for this run. Run with <code>npm run test:all</code> to generate full reports.</div>'),r.failures&&r.failures.length>0){o+=`<div class="test-section-title">Failures (${r.failures.length})</div>`;for(const t of r.failures){const s="rd-fail-"+Math.random().toString(36).slice(2),e=t.rerun_command||(null==(i=t.selector)?void 0:i.command)||"";o+=`\n <div class="test-failure-card test-failure-expandable" id="${a(s)}">\n <div class="test-failure-header" data-action="toggleFailure" data-arg="${a(s)}">\n <span class="test-failure-toggle">▶</span>\n <span class="test-failure-name">${a(t.name)}</span>\n </div>\n <div class="test-failure-detail" style="display:none">\n <div class="test-failure-file">${a(t.file)}</div>\n ${t.error?`<pre class="test-failure-error">${a(String(t.error).slice(0,500))}</pre>`:""}\n ${t.error_stack?`<pre class="test-failure-stack">${a(String(t.error_stack).split("\n").slice(0,10).join("\n"))}</pre>`:""}\n <div class="test-failure-actions">\n ${e?`<div class="test-failure-rerun">\n <code>${a(e)}</code>\n <button class="test-copy-btn" data-action="copyText" data-arg="${a(e)}" title="Copy rerun command">Copy</button>\n </div>`:""}\n </div>\n </div>\n </div>`}}if(r.skips&&r.skips.length>0){o+=`<details><summary class="test-section-title" style="cursor:pointer">Skipped (${r.skips.length})</summary>`,o+='<table class="test-groups-table"><thead><tr><th>Test</th><th>File</th></tr></thead><tbody>';for(const t of r.skips.slice(0,50))o+=`<tr><td>${a(t.name)}</td><td class="meta">${a(t.file)}</td></tr>`;r.skips.length>50&&(o+=`<tr><td colspan="2" class="meta">...and ${r.skips.length-50} more</td></tr>`),o+="</tbody></table></details>"}l.innerHTML=o,l.scrollIntoView({behavior:"smooth",block:"nearest"})}catch(r){l.innerHTML=`<div class="meta">Failed: ${a(r.message)}</div>`}}}let k=null;function x(){const t=document.getElementById("testProgressBar");t&&e("/api/tests/progress").then(s=>{var e;if(!s.running&&!s.finished)return void(t.innerHTML="");const n=((s.finished||Date.now())-s.started)/1e3,i=n>=60?(n/60).toFixed(1)+"m":Math.round(n)+"s",l=s.passed+s.failed+s.skipped,r=g[null==(e=s.suite)?void 0:e.replace("test:","")]||s.suite||"Tests";if(s.running){const e=s.current_file?s.current_file.split("/").pop():"";t.innerHTML=`\n <div class="test-progress-live">\n <div class="test-progress-live-header">\n <span class="test-progress-live-status">⏳ Running ${a(r)}...</span>\n <span class="meta">${i}</span>\n <button class="test-stop-btn" data-action="stopTests" title="Stop running tests">■ Stop</button>\n </div>\n <div class="test-progress-live-stats">\n <span class="test-color-pass">${s.passed} pass</span>\n <span class="test-color-fail">${s.failed} fail</span>\n <span class="test-color-skip">${s.skipped} skip</span>\n <span>${s.files_done}${s.files_total?"/"+s.files_total:""} files</span>\n <span>${l} tests</span>\n </div>\n ${e?`<div class="test-progress-live-file">${a(e)}</div>`:""}\n <div class="test-progress-bar" style="margin-top:6px">\n <div class="test-progress-pass" style="width:${s.files_total>0?s.files_done/s.files_total*100:l>0?s.passed/l*100:0}%;transition:width 0.3s;background:#22c55e"></div>\n <div class="test-progress-fail" style="width:${s.files_total>0?0:l>0?s.failed/l*100:0}%;transition:width 0.3s"></div>\n </div>\n </div>`}else{const e=s.failed>0?"test-color-fail":"test-color-pass",n=s.failed>0?"FAILED":"PASSED";t.innerHTML=`\n <div class="test-progress-live test-progress-done">\n <div class="test-progress-live-header">\n <span class="${e}" style="font-weight:700">✓ ${a(r)} ${n}</span>\n <span class="meta">${i}</span>\n </div>\n <div class="test-progress-live-stats">\n <span class="test-color-pass">${s.passed} pass</span>\n <span class="test-color-fail">${s.failed} fail</span>\n <span class="test-color-skip">${s.skipped} skip</span>\n <span>${s.files_done} files</span>\n </div>\n </div>`,k&&(clearInterval(k),k=null),M(),y(),b(),E(),setTimeout(()=>{t&&(t.innerHTML="")},1e4)}}).catch(()=>{})}async function S(t){try{n(`Starting ${t}...`),await i("/api/tests/run",{suite:t}),k&&clearInterval(k),x(),k=setInterval(x,2e3),L()}catch(s){n("Failed to start tests: "+s.message,!0)}}async function T(){try{(await i("/api/tests/stop",{})).stopped?(n("Tests stopped."),k&&(clearInterval(k),k=null),M(),x(),setTimeout(()=>refreshTestData(),1e3)):n("No running tests to stop.")}catch(t){n("Failed to stop tests: "+t.message,!0)}}async function I(t,s){const e={unit:"test:unit",integration:"test:integration",e2e:"test:e2e",all:"test:all",unknown:"test:unit"}[t]||"test:unit";try{n(`Running ${s.split("/").pop()}...`),await i("/api/tests/run",{suite:e,file:s}),k&&clearInterval(k),x(),k=setInterval(x,2e3),L()}catch(a){n("Failed to start test: "+a.message,!0)}}function L(){M(),p="";const t=function(){var t;let s=document.getElementById("testStreamPanel");if(!s){const e=document.getElementById("testProgressBar");if(!e)return null;s=document.createElement("div"),s.id="testStreamPanel",s.className="test-stream-panel",s.style.display="none",s.innerHTML='\n <div class="test-stream-header">\n <span>Live Output</span>\n <button class="test-stream-close" onclick="document.getElementById(\'testStreamPanel\').style.display=\'none\'">✕</button>\n </div>\n <pre id="testStreamPre" class="test-stream-pre"></pre>',null==(t=e.parentNode)||t.insertBefore(s,e.nextSibling)}return s}();t&&(t.style.display="block",t.querySelector("pre").textContent=""),d=new EventSource("/api/tests/stream"),d.onmessage=t=>{try{const s=JSON.parse(t.data);s.reset?p=s.text||"":s.text&&(p+=s.text),s.done&&M();const e=document.getElementById("testStreamPre");e&&(e.textContent=p,e.scrollTop=e.scrollHeight)}catch{}},d.onerror=()=>{M()}}function M(){d&&(d.close(),d=null)}function F(t){const s=document.getElementById(t);if(!s)return;const e=s.querySelector(".test-failure-detail"),a=s.querySelector(".test-failure-toggle");if(!e)return;const n="none"!==e.style.display;e.style.display=n?"none":"block",a&&(a.textContent=n?"▶":"▼")}async function E(){const t=document.getElementById("testingChart");if(t)try{const s=((await e("/api/tests/history")).history||[]).slice(0,20).reverse();if(0===s.length)return void(t.innerHTML="");const a=600,n=120,i=30,l=2,r=Math.max(...s.map(t=>(t.passed||0)+(t.failed||0)),1),o=Math.floor((a-2*i-l*(s.length-1))/s.length);let c="",d="";s.forEach((t,e)=>{const a=i+e*(o+l),p=(t.passed||0)+(t.failed||0),u=p>0?Math.round((t.passed||0)/r*(n-i)):0,f=p>0?Math.round((t.failed||0)/r*(n-i)):0,v=n-i-u-f;if(f>0&&(c+=`<rect x="${a}" y="${n-i-f}" width="${o}" height="${f}" fill="#ef4444" rx="1" opacity="0.85"><title>${h(t.timestamp)} — ${t.failed} fail</title></rect>`),u>0&&(c+=`<rect x="${a}" y="${v}" width="${o}" height="${u}" fill="#22c55e" rx="1" opacity="0.85"><title>${h(t.timestamp)} — ${t.passed} pass</title></rect>`),e%5==0||e===s.length-1){const s=t.timestamp?new Date(t.timestamp).toLocaleDateString(void 0,{month:"short",day:"numeric"}):"";d+=`<text x="${a+o/2}" y="${n-2}" text-anchor="middle" font-size="9" fill="var(--text-3, #888)">${s}</text>`}});const p=`<svg viewBox="0 0 ${a} ${n}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${a}px;height:${n}px;display:block">\n <line x1="${i}" y1="${n-i}" x2="${a-i}" y2="${n-i}" stroke="var(--border,#333)" stroke-width="1"/>\n ${c}\n ${d}\n </svg>`;t.innerHTML=`\n <div class="test-section-title">Run History (last ${s.length})\n <span class="test-chart-legend">\n <span style="color:#22c55e">■</span> Pass\n <span style="color:#ef4444">■</span> Fail\n </span>\n </div>\n <div class="test-chart-wrap">${p}</div>`}catch{}}function H(t){var s;null==(s=navigator.clipboard)||s.writeText(t).then(()=>{n("Copied to clipboard")}).catch(()=>{n("Copy failed",!0)})}export{T as a,S as b,H as c,o as i,w as l,I as r,u as s,F as t};
@@ -6,7 +6,7 @@
6
6
  <title>crewswarm dashboard</title>
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <!-- Font: system stack only to avoid CORS when dashboard (4319) and studio (3333) both load Inter from Google -->
9
- <script type="module" crossorigin src="/assets/index-BeVllEj_.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-CSooN9fi.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/core-utils-CmOkXgzi.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/setup-wizard-CA0Or47w.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/components-BS9fQjE_.js">
@@ -20,7 +20,7 @@
20
20
  <link rel="modulepreload" crossorigin href="/assets/tab-services-tab-DU_LH3uG.js">
21
21
  <link rel="modulepreload" crossorigin href="/assets/tab-agents-tab-BgpIsjkw.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/tab-prompts-tab-DVkUNaJd.js">
23
- <link rel="modulepreload" crossorigin href="/assets/tab-testing-tab-CezZOZcJ.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/tab-testing-tab-Ea5K-rsb.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/tab-skills-tab-DR7PJ7NB.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/tab-contacts-tab-DiOyMYth.js">
26
26
  <link rel="modulepreload" crossorigin href="/assets/tab-engines-tab-BsdZVvU0.js">
@@ -30,9 +30,9 @@
30
30
  <link rel="modulepreload" crossorigin href="/assets/tab-settings-tab-CuvH_Fj_.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/tab-comms-tab-kguqTIzD.js">
32
32
  <link rel="modulepreload" crossorigin href="/assets/tab-usage-tab-BIOOnB-Y.js">
33
- <link rel="modulepreload" crossorigin href="/assets/tab-spending-tab-DEccQHnt.js">
33
+ <link rel="modulepreload" crossorigin href="/assets/tab-spending-tab-DcXD5TQY.js">
34
34
  <link rel="modulepreload" crossorigin href="/assets/tab-pm-loop-tab-DiAPTJXu.js">
35
- <link rel="stylesheet" crossorigin href="/assets/index-D-sRshvg.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/index-C5-vlIwl.css">
36
36
  </head>
37
37
  <body>
38
38
  <!-- Skip link for keyboard navigation -->
@@ -3018,6 +3018,31 @@
3018
3018
 
3019
3019
  </div>
3020
3020
  </div>
3021
+ <div
3022
+ style="
3023
+ text-align: center;
3024
+ font-size: 20px;
3025
+ color: var(--text-3);
3026
+ line-height: 2;
3027
+ "
3028
+ >
3029
+ +
3030
+ </div>
3031
+ <div style="text-align: center">
3032
+ <div style="font-size: 11px; color: var(--text-3)">
3033
+ crew-cli
3034
+ </div>
3035
+ <div
3036
+ style="
3037
+ font-size: 20px;
3038
+ font-weight: 700;
3039
+ color: var(--purple, #a78bfa);
3040
+ "
3041
+ id="gtCrewCliCost"
3042
+ >
3043
+
3044
+ </div>
3045
+ </div>
3021
3046
  <div
3022
3047
  style="
3023
3048
  text-align: center;
@@ -3079,7 +3104,7 @@
3079
3104
  <select
3080
3105
  id="spendingDays"
3081
3106
  style="font-size: 11px; padding: 3px 6px"
3082
- data-onchange="loadSpending"
3107
+ data-onchange="loadAllUsage"
3083
3108
  >
3084
3109
  <option value="1" selected>Today</option>
3085
3110
  <option value="7">Last 7 days</option>
@@ -3204,8 +3229,9 @@
3204
3229
  <select
3205
3230
  id="ocStatsDays"
3206
3231
  style="font-size: 11px; padding: 3px 6px"
3207
- data-onchange="loadOcStats"
3232
+ data-onchange="loadAllUsage"
3208
3233
  >
3234
+ <option value="1">Today</option>
3209
3235
  <option value="7">Last 7 days</option>
3210
3236
  <option value="14" selected>Last 14 days</option>
3211
3237
  <option value="30">Last 30 days</option>
@@ -3225,6 +3251,56 @@
3225
3251
  </div>
3226
3252
  </div>
3227
3253
  </div>
3254
+
3255
+ <!-- crew-cli Usage -->
3256
+ <div class="card">
3257
+ <div
3258
+ style="
3259
+ display: flex;
3260
+ align-items: center;
3261
+ justify-content: space-between;
3262
+ margin-bottom: 10px;
3263
+ "
3264
+ >
3265
+ <div>
3266
+ <span class="card-title" style="margin: 0"
3267
+ >&#x1F6E0;&#xFE0F; crew-cli Usage</span
3268
+ >
3269
+ <span
3270
+ style="
3271
+ font-size: 11px;
3272
+ font-weight: 400;
3273
+ color: var(--text-3);
3274
+ "
3275
+ >(Direct LLM calls from crew-cli sessions)</span
3276
+ >
3277
+ </div>
3278
+ <div style="display: flex; gap: 6px; align-items: center">
3279
+ <select
3280
+ id="crewCliDays"
3281
+ style="font-size: 11px; padding: 3px 6px"
3282
+ data-onchange="loadAllUsage"
3283
+ >
3284
+ <option value="1">Today</option>
3285
+ <option value="7">Last 7 days</option>
3286
+ <option value="14" selected>Last 14 days</option>
3287
+ <option value="30">Last 30 days</option>
3288
+ </select>
3289
+ <button
3290
+ data-action="loadCrewCliStats"
3291
+ class="btn-ghost"
3292
+ style="font-size: 11px"
3293
+ >
3294
+ &#x21BB; Refresh
3295
+ </button>
3296
+ </div>
3297
+ </div>
3298
+ <div id="crewCliStatsWidget">
3299
+ <div style="color: var(--text-3); font-size: 12px">
3300
+ Loading&#x2026;
3301
+ </div>
3302
+ </div>
3303
+ </div>
3228
3304
  </div>
3229
3305
 
3230
3306
  <!-- Security: Command allowlist + Env vars -->
@@ -5578,10 +5654,12 @@
5578
5654
  </button>
5579
5655
  </div>
5580
5656
  </div>
5581
- <div id="testProgressBar"></div>
5582
5657
  <div id="testingContent">
5583
5658
  <div class="meta" style="padding: 20px">Loading test results...</div>
5584
5659
  </div>
5660
+ <div id="testProgressBar"></div>
5661
+ <div id="testingChart"></div>
5662
+ <div id="testingCoverage"></div>
5585
5663
  <div id="testingHistory"></div>
5586
5664
  </div>
5587
5665
 
Binary file
@@ -41,7 +41,16 @@ interface StatusResult {
41
41
  error?: string;
42
42
  }
43
43
 
44
- function getConfig(api: any): CrewSwarmConfig {
44
+ interface OpenClawApi {
45
+ config?: { plugins?: { entries?: { crewswarm?: { config?: CrewSwarmConfig } } } };
46
+ registerTool(def: Record<string, unknown>): void;
47
+ registerCommand(def: Record<string, unknown>): void;
48
+ registerGatewayMethod(name: string, handler: (ctx: Record<string, unknown>) => Promise<void>): void;
49
+ registerService(def: { id: string; start(): Promise<void>; stop(): void }): void;
50
+ logger?: { info(msg: string): void; warn(msg: string): void };
51
+ }
52
+
53
+ function getConfig(api: OpenClawApi): CrewSwarmConfig {
45
54
  return api.config?.plugins?.entries?.crewswarm?.config ?? {};
46
55
  }
47
56
 
@@ -74,8 +83,8 @@ async function apiDispatch(
74
83
  body: JSON.stringify(body),
75
84
  });
76
85
  return res.json() as Promise<DispatchResult>;
77
- } catch (e: any) {
78
- return { ok: false, error: `Network error: ${e.message}` };
86
+ } catch (e: unknown) {
87
+ return { ok: false, error: `Network error: ${(e as Error).message}` };
79
88
  }
80
89
  }
81
90
 
@@ -87,8 +96,8 @@ async function apiStatus(
87
96
  try {
88
97
  const res = await fetch(`${base}/api/status/${taskId}`, { headers });
89
98
  return res.json() as Promise<StatusResult>;
90
- } catch (e: any) {
91
- return { ok: false, taskId, status: "unknown", error: `Network error: ${e.message}` };
99
+ } catch (e: unknown) {
100
+ return { ok: false, taskId, status: "unknown", error: `Network error: ${(e as Error).message}` };
92
101
  }
93
102
  }
94
103
 
@@ -107,7 +116,7 @@ async function apiAgents(
107
116
 
108
117
  /** Dispatch and wait for result, polling until done or timeout */
109
118
  async function dispatchAndWait(
110
- api: any,
119
+ api: OpenClawApi,
111
120
  agent: string,
112
121
  task: string,
113
122
  verify?: string,
@@ -139,7 +148,7 @@ async function dispatchAndWait(
139
148
  return `Timeout: ${agent} did not complete within ${timeoutMs / 1000}s (taskId: ${taskId})`;
140
149
  }
141
150
 
142
- export default function register(api: any) {
151
+ export default function register(api: OpenClawApi) {
143
152
  // ── Agent tools ───────────────────────────────────────────────────────────
144
153
 
145
154
  api.registerTool({
@@ -231,7 +240,7 @@ export default function register(api: any) {
231
240
  description: "Dispatch a task to CrewSwarm. Usage: /crewswarm <agent> <task>",
232
241
  acceptsArgs: true,
233
242
  requireAuth: true,
234
- handler: async (ctx: any) => {
243
+ handler: async (ctx: { args?: string }) => {
235
244
  const args = (ctx.args ?? "").trim();
236
245
  if (!args) {
237
246
  const cfg = getConfig(api);
@@ -254,7 +263,7 @@ export default function register(api: any) {
254
263
 
255
264
  // ── Gateway RPC ───────────────────────────────────────────────────────────
256
265
 
257
- api.registerGatewayMethod("crewswarm.dispatch", async ({ params, respond }: any) => {
266
+ api.registerGatewayMethod("crewswarm.dispatch", async ({ params, respond }: { params?: Record<string, string>; respond(ok: boolean, data: Record<string, unknown>): void }) => {
258
267
  const { agent, task, verify, done } = params ?? {};
259
268
  if (!agent || !task) {
260
269
  respond(false, { error: "agent and task are required" });
@@ -265,7 +274,7 @@ export default function register(api: any) {
265
274
  respond(dispatch.ok, dispatch);
266
275
  });
267
276
 
268
- api.registerGatewayMethod("crewswarm.status", async ({ params, respond }: any) => {
277
+ api.registerGatewayMethod("crewswarm.status", async ({ params, respond }: { params?: Record<string, string>; respond(ok: boolean, data: Record<string, unknown>): void }) => {
269
278
  const { taskId } = params ?? {};
270
279
  if (!taskId) { respond(false, { error: "taskId required" }); return; }
271
280
  const cfg = getConfig(api);
@@ -273,7 +282,7 @@ export default function register(api: any) {
273
282
  respond(s.ok, s);
274
283
  });
275
284
 
276
- api.registerGatewayMethod("crewswarm.agents", async ({ respond }: any) => {
285
+ api.registerGatewayMethod("crewswarm.agents", async ({ respond }: { respond(ok: boolean, data: Record<string, unknown>): void }) => {
277
286
  const cfg = getConfig(api);
278
287
  const agents = await apiAgents(baseUrl(cfg), authHeaders(cfg));
279
288
  respond(true, { agents });
package/install.sh CHANGED
@@ -121,7 +121,7 @@ if [[ ! -f "$CONFIG_FILE" ]]; then
121
121
  }
122
122
  }
123
123
  EOF
124
- success "Created ~/.crewswarm/crewswarm.json (RT token: $RT_TOKEN)"
124
+ success "Created ~/.crewswarm/config.json (RT token: $RT_TOKEN)"
125
125
  else
126
126
  success "~/.crewswarm/crewswarm.json already exists — keeping it"
127
127
  fi
@@ -307,7 +307,7 @@ elif [[ "$SHELL" == *"bash"* ]]; then
307
307
  SHELL_RC="$HOME/.bash_profile"
308
308
  fi
309
309
 
310
- BIN_ALIAS="alias crew-cli='node $REPO_DIR/crew-cli.mjs'"
310
+ BIN_ALIAS="alias crew-cli='node $REPO_DIR/crew-cli/dist/crew.mjs'"
311
311
  if [[ -n "$SHELL_RC" ]] && ! grep -q "crew-cli" "$SHELL_RC" 2>/dev/null; then
312
312
  echo "" >> "$SHELL_RC"
313
313
  echo "# CrewSwarm" >> "$SHELL_RC"
@@ -95,6 +95,73 @@ function classifyFailureReason(text = "") {
95
95
  return "generic_failure";
96
96
  }
97
97
 
98
+ function isVerificationCommand(command = "") {
99
+ const text = String(command || "").trim().toLowerCase();
100
+ if (!text) return false;
101
+ return (
102
+ /\b(node\s+--test|npm\s+test|npm\s+run\s+test|pnpm\s+test|pnpm\s+run\s+test|yarn\s+test|pytest|go\s+test|cargo\s+test|bun\s+test)\b/.test(text) ||
103
+ /\b(tsc\b|tsc\s+--noemit|npm\s+run\s+build|pnpm\s+build|yarn\s+build|vite\s+build|next\s+build|npm\s+run\s+lint|pnpm\s+lint|yarn\s+lint)\b/.test(text)
104
+ );
105
+ }
106
+
107
+ function clamp01(value) {
108
+ if (!Number.isFinite(value)) return 0;
109
+ return Math.max(0, Math.min(1, value));
110
+ }
111
+
112
+ export function scoreTaskTrajectory(trace = {}) {
113
+ const actions = Array.isArray(trace.actions) ? trace.actions : [];
114
+ const commands = actions.filter((action) => action?.tool === "run_cmd");
115
+ const verificationCommands = commands.filter((action) => isVerificationCommand(action.command));
116
+ const writeActions = actions.filter((action) => action?.tool === "write_file" || action?.tool === "append_file");
117
+ const readActions = actions.filter((action) => action?.tool === "read_file");
118
+
119
+ const commandPrefixCounts = new Map();
120
+ const targetCounts = new Map();
121
+ for (const action of actions) {
122
+ if (action?.commandPrefix) {
123
+ commandPrefixCounts.set(action.commandPrefix, (commandPrefixCounts.get(action.commandPrefix) || 0) + 1);
124
+ }
125
+ if (action?.target) {
126
+ targetCounts.set(action.target, (targetCounts.get(action.target) || 0) + 1);
127
+ }
128
+ }
129
+
130
+ const repeatedCommandPrefixes = [...commandPrefixCounts.values()].filter((count) => count > 1).length;
131
+ const repeatedTargets = [...targetCounts.values()].filter((count) => count > 1).length;
132
+ const uniqueTools = new Set(actions.map((action) => action?.tool).filter(Boolean)).size;
133
+ const readBeforeWriteRatio = writeActions.length === 0
134
+ ? 1
135
+ : clamp01(readActions.length / writeActions.length);
136
+ const verificationScore = commands.length === 0
137
+ ? 0
138
+ : clamp01(verificationCommands.length / commands.length);
139
+ const churnPenalty = clamp01((repeatedCommandPrefixes * 0.12) + (repeatedTargets * 0.08));
140
+ const diversityScore = clamp01(uniqueTools / 4);
141
+
142
+ let score = 0;
143
+ score += trace.success ? 0.45 : 0.15;
144
+ score += verificationScore * 0.20;
145
+ score += readBeforeWriteRatio * 0.20;
146
+ score += diversityScore * 0.15;
147
+ score -= churnPenalty;
148
+
149
+ return {
150
+ actionCount: actions.length,
151
+ commandCount: commands.length,
152
+ verificationCommandCount: verificationCommands.length,
153
+ hasVerification: verificationCommands.length > 0,
154
+ writeCount: writeActions.length,
155
+ readCount: readActions.length,
156
+ uniqueToolCount: uniqueTools,
157
+ repeatedCommandPrefixes,
158
+ repeatedTargets,
159
+ readBeforeWriteRatio: Number(readBeforeWriteRatio.toFixed(3)),
160
+ verificationScore: Number(verificationScore.toFixed(3)),
161
+ trajectoryScore: Number(clamp01(score).toFixed(3)),
162
+ };
163
+ }
164
+
98
165
  export function getAutoHarnessPaths(agentId, projectId = "global") {
99
166
  const rootDir = resolveAutoHarnessRoot();
100
167
  if (!rootDir) return null;
@@ -174,11 +241,17 @@ export function recordTaskTrace({
174
241
  error,
175
242
  engineUsed,
176
243
  success,
244
+ metrics,
177
245
  }) {
178
246
  if (!agentId) return;
179
247
  const paths = getAutoHarnessPaths(agentId, projectId);
180
248
  if (!paths) return;
181
249
  const { taskTraceFile } = paths;
250
+ const actions = extractToolActions(reply);
251
+ const derivedMetrics = scoreTaskTrajectory({
252
+ success: Boolean(success),
253
+ actions,
254
+ });
182
255
  appendJsonl(taskTraceFile, {
183
256
  ts: new Date().toISOString(),
184
257
  agentId,
@@ -191,7 +264,10 @@ export function recordTaskTrace({
191
264
  errorClass: classifyFailureReason(error),
192
265
  engineUsed: engineUsed || null,
193
266
  success: Boolean(success),
194
- actions: extractToolActions(reply),
267
+ actions,
268
+ metrics: metrics && typeof metrics === "object"
269
+ ? { ...derivedMetrics, ...metrics }
270
+ : derivedMetrics,
195
271
  });
196
272
  }
197
273
 
@@ -342,6 +418,7 @@ export function scoreHarness(agentId, projectId = "global") {
342
418
  }
343
419
  const { toolTraceFile } = paths;
344
420
  const traces = loadJsonl(toolTraceFile);
421
+ const taskTraces = loadJsonl(paths.taskTraceFile);
345
422
 
346
423
  const stats = {
347
424
  traces: traces.length,
@@ -380,12 +457,85 @@ export function scoreHarness(agentId, projectId = "global") {
380
457
  const recall =
381
458
  stats.badOutcomes > 0 ? stats.blockedBadOutcomes / stats.badOutcomes : 0;
382
459
 
460
+ const taskMetrics = taskTraces
461
+ .map((trace) => trace?.metrics && typeof trace.metrics === "object"
462
+ ? trace.metrics
463
+ : scoreTaskTrajectory(trace))
464
+ .filter(Boolean);
465
+
466
+ const taskStats = {
467
+ tasks: taskMetrics.length,
468
+ avgTrajectoryScore: taskMetrics.length
469
+ ? Number((taskMetrics.reduce((sum, item) => sum + Number(item.trajectoryScore || 0), 0) / taskMetrics.length).toFixed(3))
470
+ : 0,
471
+ verificationRate: taskMetrics.length
472
+ ? Number((taskMetrics.filter((item) => item.hasVerification).length / taskMetrics.length).toFixed(3))
473
+ : 0,
474
+ avgReadBeforeWriteRatio: taskMetrics.length
475
+ ? Number((taskMetrics.reduce((sum, item) => sum + Number(item.readBeforeWriteRatio || 0), 0) / taskMetrics.length).toFixed(3))
476
+ : 0,
477
+ };
478
+
383
479
  return {
384
480
  harness,
385
481
  stats: {
386
482
  ...stats,
387
483
  precision: Number(precision.toFixed(3)),
388
484
  recall: Number(recall.toFixed(3)),
485
+ taskStats,
389
486
  },
390
487
  };
391
488
  }
489
+
490
+ /**
491
+ * Extract trajectory feedback from task traces for the adaptive weight system.
492
+ * Returns data in the format expected by action-ranking.ts loadAdaptiveWeights().
493
+ */
494
+ export function extractTrajectoryFeedback(agentId, projectId = "global") {
495
+ const paths = getAutoHarnessPaths(agentId, projectId);
496
+ if (!paths) return [];
497
+
498
+ const taskTraces = loadJsonl(paths.taskTraceFile);
499
+ if (!taskTraces.length) return [];
500
+
501
+ const READ_TOOLS = new Set(["read_file", "read_many_files", "glob", "grep_search", "list_directory", "lsp"]);
502
+ const SEARCH_TOOLS = new Set(["grep_search", "glob", "search_files", "find_definition"]);
503
+ const EDIT_TOOLS = new Set(["replace", "edit", "append_file", "write_file", "notebook_edit"]);
504
+ const SHELL_TOOLS = new Set(["run_shell_command", "shell", "run_cmd", "check_background_task"]);
505
+
506
+ function classifyAction(tool) {
507
+ if (READ_TOOLS.has(tool)) return "read";
508
+ if (SEARCH_TOOLS.has(tool)) return "search";
509
+ if (EDIT_TOOLS.has(tool)) return "edit";
510
+ if (SHELL_TOOLS.has(tool)) return "verify";
511
+ return null;
512
+ }
513
+
514
+ function detectMode(task = "") {
515
+ const t = task.toLowerCase();
516
+ if (/(failing tests?|test failure|fix tests?|fix the test|test.*(fail|broken))/.test(t)) return "test_repair";
517
+ if (/(fix|bug|broken|error|regression|crash)/.test(t)) return "bugfix";
518
+ if (/(refactor|cleanup|restructure|rename|simplify)/.test(t)) return "refactor";
519
+ if (/(add|implement|create|build|support|introduce)/.test(t)) return "feature";
520
+ return "analysis";
521
+ }
522
+
523
+ return taskTraces.map((trace) => {
524
+ const actions = Array.isArray(trace.actions) ? trace.actions : [];
525
+ const total = actions.length || 1;
526
+ const dist = { read: 0, search: 0, edit: 0, test: 0, build: 0, verify: 0, delegate: 0 };
527
+
528
+ for (const action of actions) {
529
+ const type = classifyAction(action?.tool);
530
+ if (type && type in dist) dist[type] += 1 / total;
531
+ }
532
+
533
+ const metrics = trace.metrics || scoreTaskTrajectory(trace);
534
+ return {
535
+ mode: detectMode(trace.task || trace.agentId || ""),
536
+ score: Number(metrics.trajectoryScore || 0),
537
+ toolDistribution: dist,
538
+ success: Boolean(trace.success),
539
+ };
540
+ });
541
+ }
@@ -9,7 +9,7 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import { getStatePath } from "../runtime/paths.mjs";
11
11
 
12
- const MAX_HISTORY = 2000;
12
+ const MAX_HISTORY = 40;
13
13
 
14
14
  function getHistoryDir() {
15
15
  const dir = getStatePath("chat-history");
@@ -8,6 +8,7 @@
8
8
  import { existsSync, mkdirSync } from 'fs';
9
9
  import { dirname, join } from 'path';
10
10
  import { homedir } from 'os';
11
+ import { getStatePath } from '../runtime/paths.mjs';
11
12
 
12
13
  // Try to import better-sqlite3, but make it optional
13
14
  let Database;
@@ -27,7 +28,7 @@ function getDb() {
27
28
 
28
29
  if (_db) return _db;
29
30
 
30
- const dbPath = join(homedir(), '.crewswarm', 'contacts.db');
31
+ const dbPath = process.env.CREWSWARM_CONTACTS_DB_PATH || getStatePath('contacts.db');
31
32
  const dir = dirname(dbPath);
32
33
 
33
34
  if (!existsSync(dir)) {
@@ -40,8 +41,28 @@ function getDb() {
40
41
  }
41
42
 
42
43
  function initSchema(db) {
43
- // Schema already created by lib/contacts/index.mjs
44
- // Just ensure platform_links column exists (added in migration)
44
+ db.exec(`
45
+ CREATE TABLE IF NOT EXISTS contacts (
46
+ contact_id TEXT PRIMARY KEY,
47
+ platform TEXT NOT NULL,
48
+ display_name TEXT,
49
+ phone_number TEXT,
50
+ email TEXT,
51
+ avatar_url TEXT,
52
+ preferences TEXT,
53
+ tags TEXT,
54
+ notes TEXT,
55
+ platform_links TEXT,
56
+ first_seen INTEGER NOT NULL,
57
+ last_seen INTEGER NOT NULL,
58
+ message_count INTEGER DEFAULT 0,
59
+ last_location TEXT,
60
+ timezone TEXT,
61
+ language TEXT DEFAULT 'en'
62
+ );
63
+ `);
64
+
65
+ // Just ensure platform_links column exists for older DBs.
45
66
  try {
46
67
  db.exec(`
47
68
  ALTER TABLE contacts ADD COLUMN platform_links TEXT;
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
12
12
  import { existsSync, mkdirSync } from 'fs';
13
13
  import { dirname, join } from 'path';
14
14
  import { homedir } from 'os';
15
+ import { getStatePath } from '../runtime/paths.mjs';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
 
@@ -37,7 +38,7 @@ function getDb() {
37
38
 
38
39
  if (_db) return _db;
39
40
 
40
- const dbPath = join(homedir(), '.crewswarm', 'contacts.db');
41
+ const dbPath = process.env.CREWSWARM_CONTACTS_DB_PATH || getStatePath('contacts.db');
41
42
  const dir = dirname(dbPath);
42
43
 
43
44
  if (!existsSync(dir)) {