claude-code-session-manager 0.93.0 → 0.94.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 (67) hide show
  1. package/dist/assets/{DataModel-DDGx3qrn.js → DataModel-B0LDnnSL.js} +1 -1
  2. package/dist/assets/{History-CHBjYZyD.js → History-C685Kytt.js} +2 -2
  3. package/dist/assets/{Hooks-DG4c-Rzz.js → Hooks-CwLnp0Z_.js} +3 -3
  4. package/dist/assets/HostBilko-CwEHEKYk.js +1 -0
  5. package/dist/assets/{Library-CN1h5rAE.js → Library-BU9np05E.js} +25 -25
  6. package/dist/assets/{MarkdownEditor-DmBxrhdn.js → MarkdownEditor-B74ogK2f.js} +1 -1
  7. package/dist/assets/McpServers-BLM3ftzP.js +2 -0
  8. package/dist/assets/{Memory-Dk4uDCjg.js → Memory-DAP1t8bA.js} +6 -6
  9. package/dist/assets/{Permissions-BovobeZw.js → Permissions-3pGLJmxQ.js} +3 -3
  10. package/dist/assets/{Plugins-DvOf_B62.js → Plugins-Q1KoLEzn.js} +2 -2
  11. package/dist/assets/{ProvenanceBadge-Dutz_4OY.js → ProvenanceBadge-BnjBQiV9.js} +1 -1
  12. package/dist/assets/{SaveBar-CT5u6ZCJ.js → SaveBar-Zq2NfcPf.js} +1 -1
  13. package/dist/assets/{Scheduler-B7GSnZj5.js → Scheduler-D488ebKm.js} +9 -9
  14. package/dist/assets/{ScopeSwitcher-BUYDpz2v.js → ScopeSwitcher-Cnojv22D.js} +1 -1
  15. package/dist/assets/{Settings-DIRmr3tL.js → Settings-D5Lhj7ga.js} +3 -3
  16. package/dist/assets/{SkillReferenceGraph-CdGr6d3_.js → SkillReferenceGraph-BRDiE6Hv.js} +2 -2
  17. package/dist/assets/{Skills-BQOheQv4.js → Skills-BQDqp-EN.js} +2 -2
  18. package/dist/assets/{SystemPrompt-5LsimKbX.js → SystemPrompt-tFr19Od6.js} +1 -1
  19. package/dist/assets/{TagLibrary-DreLsT3n.js → TagLibrary-BjyrnaRE.js} +1 -1
  20. package/dist/assets/{TiptapBody-C9kmlBnI.js → TiptapBody-DGzSg3BO.js} +1 -1
  21. package/dist/assets/{Toggle-BKLCBu9A.js → Toggle-DzoROibf.js} +1 -1
  22. package/dist/assets/{index-Db_SK9rW.js → index-CCl4tz-u.js} +821 -821
  23. package/dist/assets/{settingsSchema-B5jBhSUP.js → settingsSchema-BYKVe1WM.js} +1 -1
  24. package/dist/index.html +1 -1
  25. package/package.json +6 -3
  26. package/scripts/README.md +3 -0
  27. package/src/main/__tests__/chat-cancel-terminal.test.cjs +5 -2
  28. package/src/main/__tests__/chat-exit-close-race.test.cjs +8 -2
  29. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +5 -2
  30. package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
  31. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
  32. package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
  33. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
  34. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
  35. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
  36. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
  37. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
  38. package/src/main/__tests__/runVerify.test.cjs +12 -1
  39. package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
  40. package/src/main/build-info.json +4 -4
  41. package/src/main/chatRunner.cjs +45 -10
  42. package/src/main/git.cjs +9 -2
  43. package/src/main/historyAggregator.cjs +2 -19
  44. package/src/main/index.cjs +7 -8
  45. package/src/main/ipcSchemas.cjs +13 -3
  46. package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
  47. package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
  48. package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
  49. package/src/main/lib/agentPersonaSchema.cjs +2 -0
  50. package/src/main/lib/childWithLog.cjs +92 -51
  51. package/src/main/lib/delegationReadiness.cjs +3 -1
  52. package/src/main/lib/epicSpawnPlan.cjs +27 -8
  53. package/src/main/lib/epicTranscriptPath.cjs +5 -1
  54. package/src/main/lib/headTailBuffer.cjs +43 -0
  55. package/src/main/lib/intradayRefresh.cjs +33 -0
  56. package/src/main/lib/lruCache.cjs +39 -0
  57. package/src/main/lib/openExternalApp.cjs +27 -9
  58. package/src/main/lib/opsErrorLog.cjs +22 -0
  59. package/src/main/lib/promptSessionSchema.cjs +2 -0
  60. package/src/main/lib/rendererRecovery.cjs +141 -0
  61. package/src/main/lib/scheduleJobSchema.cjs +3 -0
  62. package/src/main/runVerify.cjs +3 -1
  63. package/src/main/scheduler.cjs +360 -257
  64. package/src/main/transcripts.cjs +54 -11
  65. package/src/preload/api.d.ts +3 -1
  66. package/dist/assets/HostBilko-DHXcpV2c.js +0 -1
  67. package/dist/assets/McpServers-Beb_HD6I.js +0 -2
@@ -1 +1 @@
1
- import{ay as b,aj as n,S as N,aN as O,aw as K}from"./index-Db_SK9rW.js";function T({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=B(K)),$}export{T as E,V as s,G as u};
1
+ import{az as b,ak as n,S as N,aO as O,ax as K}from"./index-CCl4tz-u.js";function T({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function z(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:B([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function B(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=z(K)),$}export{T as E,V as s,G as u};
package/dist/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-Db_SK9rW.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-CCl4tz-u.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
13
  <link rel="stylesheet" crossorigin href="./assets/index-CKH5Uxik.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -33,7 +33,7 @@
33
33
  "dev:renderer": "vite",
34
34
  "dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
35
35
  "build": "vite build",
36
- "typecheck": "tsc --noEmit",
36
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.main.json",
37
37
  "start": "electron .",
38
38
  "postinstall": "node scripts/postinstall.cjs",
39
39
  "prepack": "node scripts/write-build-info.cjs",
@@ -53,7 +53,10 @@
53
53
  "lint:unregistered-tests": "node scripts/check-unregistered-tests.cjs",
54
54
  "lint:docs": "node scripts/check-doc-hierarchy.cjs",
55
55
  "lint:paths": "node scripts/check-ops-path-literals.cjs",
56
- "lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:unregistered-tests && npm run lint:docs && npm run lint:paths",
56
+ "lint:any": "node scripts/check-explicit-any.cjs",
57
+ "lint:main-ts-check": "node scripts/check-main-ts-check.cjs",
58
+ "lint:filenames": "node scripts/check-tracked-filenames.cjs",
59
+ "lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:unregistered-tests && npm run lint:docs && npm run lint:paths && npm run lint:any && npm run lint:main-ts-check && npm run lint:filenames",
57
60
  "sync:settings-schema": "node scripts/sync-settings-schema.cjs"
58
61
  },
59
62
  "keywords": [
package/scripts/README.md CHANGED
@@ -12,6 +12,9 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
12
12
  | check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
13
13
  | check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
14
14
  | check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
15
+ | check-explicit-any.cjs | `npm run lint:any` (in `lint`) — bans explicit `any`/`@ts-ignore` in renderer+preload; per-line `// lint-allow-any: <reason>` | no |
16
+ | check-main-ts-check.cjs | `npm run lint:main-ts-check` (in `lint`) — `tsconfig.main.json` `include` ⇔ `// @ts-check` first line, both directions; explicit list only, `checkJs` false | no |
17
+ | check-tracked-filenames.cjs | `npm run lint:filenames` (in `lint`) — fails on tracked paths with Windows-illegal chars, trailing dot/space, or reserved device names | no |
15
18
  | check-unregistered-tests.cjs | `npm run lint:unregistered-tests` (in `lint`) | no |
16
19
  | check-unstable-selectors.cjs | `npm run lint:selectors` (in `lint`) | no |
17
20
  | cleanup-nested-queue-stubs.cjs | manual one-shot; dispatches to cleanup-worktree-ops-stubs.cjs | no |
@@ -37,6 +37,9 @@ process.env.SM_CLAUDE_BIN = stubPath;
37
37
 
38
38
  const cr = require('../chatRunner.cjs');
39
39
 
40
+ // Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
41
+ const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
42
+
40
43
  const tick = () => new Promise((r) => setImmediate(r));
41
44
 
42
45
  test('cancelling an ACTIVE run broadcasts a terminal event (unsticks the UI)', async () => {
@@ -50,7 +53,7 @@ test('cancelling an ACTIVE run broadcasts a terminal event (unsticks the UI)', a
50
53
  },
51
54
  });
52
55
 
53
- cr.run({ tabId: 'T', sessionId: 'S', prompt: 'hello', cwd: process.cwd(), resume: false });
56
+ cr.run({ tabId: 'T', sessionId: 'S', prompt: 'hello', cwd: scratchCwd, resume: false });
54
57
 
55
58
  // Let the spawn happen and started event fire.
56
59
  for (let i = 0; i < 6; i++) await tick();
@@ -86,7 +89,7 @@ test('cancel() resolves only AFTER the terminal broadcast fires, not immediately
86
89
  },
87
90
  });
88
91
 
89
- cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'hello', cwd: process.cwd(), resume: false });
92
+ cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'hello', cwd: scratchCwd, resume: false });
90
93
 
91
94
  for (let i = 0; i < 6; i++) await tick();
92
95
 
@@ -32,6 +32,9 @@ import { test, afterAll } from 'vitest';
32
32
  const assert = require('node:assert/strict');
33
33
  const EventEmitter = require('node:events');
34
34
  const cp = require('node:child_process');
35
+ const fs = require('node:fs');
36
+ const os = require('node:os');
37
+ const path = require('node:path');
35
38
 
36
39
  // Replace spawn BEFORE chatRunner.cjs is first required, so the module's
37
40
  // top-level `const { spawn } = require('node:child_process')` destructuring
@@ -50,6 +53,9 @@ cp.spawn = () => {
50
53
 
51
54
  const cr = require('../chatRunner.cjs');
52
55
 
56
+ // Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
57
+ const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
58
+
53
59
  const tick = () => new Promise((r) => setImmediate(r));
54
60
 
55
61
  test('a final-text-only turn whose stdout chunk lands after exit still surfaces chat:run:complete', async () => {
@@ -63,7 +69,7 @@ test('a final-text-only turn whose stdout chunk lands after exit still surfaces
63
69
  },
64
70
  });
65
71
 
66
- cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'show me a sample post', cwd: process.cwd(), resume: false });
72
+ cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'show me a sample post', cwd: scratchCwd, resume: false });
67
73
 
68
74
  for (let i = 0; i < 20 && !nextChild; i++) await tick();
69
75
  const child = nextChild;
@@ -108,7 +114,7 @@ test('a crashed process with no result event ever emitted still fires the fallba
108
114
  },
109
115
  });
110
116
 
111
- cr.run({ tabId: 'T3', sessionId: 'S3', prompt: 'anything', cwd: process.cwd(), resume: false });
117
+ cr.run({ tabId: 'T3', sessionId: 'S3', prompt: 'anything', cwd: scratchCwd, resume: false });
112
118
 
113
119
  for (let i = 0; i < 20 && !nextChild; i++) await tick();
114
120
  const child = nextChild;
@@ -18,6 +18,9 @@ const assert = require('node:assert/strict');
18
18
  const fs = require('node:fs');
19
19
  const os = require('node:os');
20
20
  const path = require('node:path');
21
+
22
+ // Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
23
+ const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
21
24
  const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
22
25
 
23
26
  function writeStub(lines) {
@@ -70,7 +73,7 @@ test('MCP consent denial in a tool_result surfaces a chat:run:notice event', asy
70
73
  },
71
74
  });
72
75
 
73
- cr.run({ tabId: 'T-notice', sessionId: 'S-notice', prompt: 'do a design thing', cwd: process.cwd(), resume: false });
76
+ cr.run({ tabId: 'T-notice', sessionId: 'S-notice', prompt: 'do a design thing', cwd: scratchCwd, resume: false });
74
77
 
75
78
  for (let i = 0; i < 60 && !events.some((e) => isTerminal(e.channel)); i++) {
76
79
  await new Promise((r) => setTimeout(r, 25));
@@ -120,7 +123,7 @@ test('a normal tool_result with no consent marker does not fire chat:run:notice'
120
123
  },
121
124
  });
122
125
 
123
- cr.run({ tabId: 'T-clean', sessionId: 'S-clean', prompt: 'do a normal thing', cwd: process.cwd(), resume: false });
126
+ cr.run({ tabId: 'T-clean', sessionId: 'S-clean', prompt: 'do a normal thing', cwd: scratchCwd, resume: false });
124
127
 
125
128
  for (let i = 0; i < 60 && !events.some((e) => isTerminal(e.channel)); i++) {
126
129
  await new Promise((r) => setTimeout(r, 25));
@@ -0,0 +1,39 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ const { createIntradayRefresh } = require('../lib/intradayRefresh.cjs');
3
+
4
+ const mk = (over = {}) => {
5
+ const logs = { writeLine: vi.fn() };
6
+ const releaseLock = vi.fn();
7
+ const deps = { tryAcquireLock: vi.fn(() => true), releaseLock, lockPath: '/x.lock', refresh: vi.fn(async () => {}), logs, ...over };
8
+ return { deps, run: createIntradayRefresh(deps), logs, releaseLock };
9
+ };
10
+ const flush = () => new Promise((r) => setTimeout(r, 0));
11
+
12
+ test('sync throw from refresh releases the lock and logs', async () => {
13
+ const { run, logs, releaseLock } = mk({ refresh: () => { throw new Error('boom'); } });
14
+ expect(() => run()).not.toThrow();
15
+ await flush();
16
+ expect(releaseLock).toHaveBeenCalledWith('/x.lock');
17
+ expect(logs.writeLine).toHaveBeenCalled();
18
+ });
19
+
20
+ test('async rejection releases the lock', async () => {
21
+ const { run, releaseLock } = mk({ refresh: async () => { throw new Error('x'); } });
22
+ run(); await flush();
23
+ expect(releaseLock).toHaveBeenCalledTimes(1);
24
+ });
25
+
26
+ test('lock-acquire throw is logged, not escaped, and no release', async () => {
27
+ const { run, logs, releaseLock, deps } = mk({ tryAcquireLock: () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); } });
28
+ expect(() => run()).not.toThrow();
29
+ expect(logs.writeLine).toHaveBeenCalledTimes(1);
30
+ expect(releaseLock).not.toHaveBeenCalled();
31
+ expect(deps.refresh).not.toHaveBeenCalled();
32
+ });
33
+
34
+ test('contended lock skips refresh', () => {
35
+ const { run, deps, releaseLock } = mk({ tryAcquireLock: () => false });
36
+ run();
37
+ expect(deps.refresh).not.toHaveBeenCalled();
38
+ expect(releaseLock).not.toHaveBeenCalled();
39
+ });
@@ -0,0 +1,25 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ const Module = require('node:module');
3
+ const origLoad = Module._load;
4
+ Module._load = function (req, ...rest) {
5
+ if (req === 'electron') return { shell: { openPath: async () => '' } };
6
+ return origLoad.call(this, req, ...rest);
7
+ };
8
+ const { spawnDetached } = require('../lib/openExternalApp.cjs');
9
+ Module._load = origLoad;
10
+
11
+ test('non-existent command resolves ok:false with no uncaught exception', async () => {
12
+ const onUncaught = vi.fn();
13
+ process.on('uncaughtException', onUncaught);
14
+ const r = await spawnDetached('sm-definitely-not-a-real-binary-xyz', ['/tmp']);
15
+ await new Promise((res) => setTimeout(res, 20));
16
+ process.off('uncaughtException', onUncaught);
17
+ expect(r.ok).toBe(false);
18
+ expect(r.error).toMatch(/failed to launch/);
19
+ expect(onUncaught).not.toHaveBeenCalled();
20
+ });
21
+
22
+ test('real command resolves ok:true with opener name', async () => {
23
+ const r = await spawnDetached('true', []);
24
+ expect(r).toEqual({ ok: true, opener: 'true' });
25
+ });
@@ -85,3 +85,25 @@ test('appendError merges caller tags without duplicating auto-derived ones', ()
85
85
  assert.equal(count, 1);
86
86
  assert.ok(line.tags.includes('silent-probe'));
87
87
  });
88
+
89
+ test('under vitest, a real project root is refused: no file written, no telemetry', () => {
90
+ const repoRoot = path.resolve(__dirname, '..', '..', '..');
91
+ const logsBefore = fs.existsSync(path.join(repoRoot, 'session-manager-operations', 'logs'))
92
+ ? fs.readdirSync(path.join(repoRoot, 'session-manager-operations', 'logs')).map((f) => [f, fs.statSync(path.join(repoRoot, 'session-manager-operations', 'logs', f)).mtimeMs])
93
+ : null;
94
+ const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
95
+ const saved = require.cache[telemetryPath];
96
+ let calls = 0;
97
+ require.cache[telemetryPath] = { id: telemetryPath, filename: telemetryPath, loaded: true, exports: { reportError: () => { calls++; }, logLine: () => { calls++; } } };
98
+ try {
99
+ opsErrorLog.appendError({ cwd: repoRoot, scope: 'chatRunner', tabId: 'T', message: 'fixture' });
100
+ opsErrorLog.appendError({ cwd: repoRoot, scope: 'chatRunner', level: 'warn', message: 'fixture' });
101
+ } finally {
102
+ if (saved) require.cache[telemetryPath] = saved; else delete require.cache[telemetryPath];
103
+ }
104
+ const logsAfter = fs.existsSync(path.join(repoRoot, 'session-manager-operations', 'logs'))
105
+ ? fs.readdirSync(path.join(repoRoot, 'session-manager-operations', 'logs')).map((f) => [f, fs.statSync(path.join(repoRoot, 'session-manager-operations', 'logs', f)).mtimeMs])
106
+ : null;
107
+ assert.deepEqual(logsAfter, logsBefore);
108
+ assert.equal(calls, 0);
109
+ });
@@ -259,3 +259,24 @@ test('spawn() still refuses session_unreachable without any node-pty spawn', ()
259
259
  expect(result.pid).toBeNull();
260
260
  expect(result.error).toMatch(/cannot be resumed/);
261
261
  });
262
+
263
+ test('planEpicSpawn with a REAL on-disk index: project-root caller resumes, vanished-worktree caller is refused spawn_cwd_missing', () => {
264
+ const { planEpicSpawn, __resetForTests } = require('../lib/epicSpawnPlan.cjs');
265
+ __resetForTests();
266
+ const projectRoot = fs.mkdtempSync(path.join(tmpHome, 'sm-plan-root-'));
267
+ const deadWt = path.join(os.tmpdir(), 'sm-plan-dead-worktree-xyz');
268
+ const sessionId = 'epic-session-root-probe';
269
+ writeActiveIndexWithWorktree(projectRoot, { epicId: 'epic-rp', claudeSessionId: sessionId, worktreeDir: deadWt });
270
+ const tdir = path.join(tmpHome, '.claude', 'projects', projectRoot.replace(/[^a-zA-Z0-9]/g, '-'));
271
+ fs.mkdirSync(tdir, { recursive: true });
272
+ fs.writeFileSync(path.join(tdir, `${sessionId}.jsonl`), '{"type":"user"}\n');
273
+ const deps = { homeDir: tmpHome, restoreWorktree: () => null };
274
+
275
+ expect(planEpicSpawn({ cwd: projectRoot, claudeSessionId: sessionId, deps }))
276
+ .toMatchObject({ ok: true, execCwd: projectRoot, useResume: true });
277
+ __resetForTests();
278
+ const refused = planEpicSpawn({ cwd: deadWt, claudeSessionId: sessionId, deps });
279
+ expect(refused).toMatchObject({ ok: false, code: 'spawn_cwd_missing' });
280
+ expect(refused.message).toContain(deadWt);
281
+ expect(refused.message).toMatch(/project cwd/);
282
+ });
@@ -20,6 +20,8 @@ const {
20
20
  nextBackoffMs,
21
21
  shouldWarnFailureStreak,
22
22
  shouldEscalateFailureStreak,
23
+ restoreFailureStreakWarnedAt,
24
+ persistedStreakMinutes,
23
25
  BACKOFF_MAX_MS,
24
26
  FAILURE_STREAK_WARN_THRESHOLD,
25
27
  FAILURE_STREAK_ESCALATION_MS,
@@ -112,3 +114,15 @@ test('shouldEscalateFailureStreak: re-arms after a streak clears (lastEscalatedA
112
114
  const laterStreakNow = firstStreakNow + 5 * 60_000; // well under 30 min later
113
115
  expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, laterStreakNow)).toBe(true);
114
116
  });
117
+
118
+ test('restoreFailureStreakWarnedAt backfills now when warned=true has no numeric timestamp', () => {
119
+ expect(restoreFailureStreakWarnedAt(true, null, 5000)).toBe(5000);
120
+ expect(restoreFailureStreakWarnedAt(true, 1000, 5000)).toBe(1000);
121
+ expect(restoreFailureStreakWarnedAt(false, null, 5000)).toBeNull();
122
+ });
123
+
124
+ test('persistedStreakMinutes is always a number, never null', () => {
125
+ expect(persistedStreakMinutes(null, 5000)).toBe(0);
126
+ expect(persistedStreakMinutes(0, 120_000)).toBe(2);
127
+ expect(persistedStreakMinutes(restoreFailureStreakWarnedAt(true, undefined, 5000), 5000)).toBe(0);
128
+ });
@@ -0,0 +1,26 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const src = fs.readFileSync(path.join(__dirname, '..', 'runVerify.cjs'), 'utf8');
5
+
6
+ test('verdicts sidecar write goes through config.writeJsonSync, not bare writeFileSync', () => {
7
+ expect(src).not.toMatch(/writeFileSync\(verdictsPath/);
8
+ expect(src).toMatch(/config\.cjs'\)\.writeJsonSync\(verdictsPath/);
9
+ });
10
+
11
+ test('conclude() invokes the atomic helper at runtime', async () => {
12
+ const config = require('../config.cjs');
13
+ const spy = vi.spyOn(config, 'writeJsonSync').mockReturnValue({ ok: true, mtimeMs: 0 });
14
+ const { verifyRun } = require('../runVerify.cjs');
15
+ const os = require('node:os');
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rv-atomic-'));
17
+ try {
18
+ try {
19
+ await verifyRun({ queueEntry: { slug: 's' }, runDir: dir, prdPath: path.join(dir, 'none.md'), exitCode: 0 });
20
+ } catch { /* signature drift is fine; assert only on the spy */ }
21
+ expect(spy.mock.calls.some(([p]) => p === path.join(dir, 's.verdicts.json'))).toBe(true);
22
+ } finally {
23
+ spy.mockRestore();
24
+ fs.rmSync(dir, { recursive: true, force: true });
25
+ }
26
+ });
@@ -32,7 +32,7 @@
32
32
  * Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-landed-commit-outranks.test.cjs
33
33
  */
34
34
 
35
- import { test } from 'vitest';
35
+ import { test, beforeAll, afterAll, vi } from 'vitest';
36
36
  const assert = require('node:assert/strict');
37
37
  const os = require('node:os');
38
38
  const fs = require('node:fs');
@@ -40,6 +40,16 @@ const path = require('node:path');
40
40
  const { execFileSync } = require('node:child_process');
41
41
  const { verifyRun } = require('../runVerify.cjs');
42
42
 
43
+ // Sidecar goes through config.writeJsonSync, whose allow-list rejects os.tmpdir() fixtures.
44
+ let writeSpy;
45
+ beforeAll(() => {
46
+ writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
47
+ require('node:fs').writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
48
+ return { ok: true, mtimeMs: 0 };
49
+ });
50
+ });
51
+ afterAll(() => writeSpy.mockRestore());
52
+
43
53
  const SLUG = '1218-fo-01-move-scripts-lib-into-src-main-lib';
44
54
  const FIXTURE_PATH = path.join(__dirname, 'fixtures', `${SLUG}.log`);
45
55
  const LANDED_COMMIT = 'd1edf15be50a783f08a55e6de328780f0feb3cb0';
@@ -14,7 +14,7 @@
14
14
  * Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs
15
15
  */
16
16
 
17
- import { test } from 'vitest';
17
+ import { test, beforeAll, afterAll, vi } from 'vitest';
18
18
  const assert = require('node:assert/strict');
19
19
  const os = require('node:os');
20
20
  const fs = require('node:fs');
@@ -22,6 +22,16 @@ const path = require('node:path');
22
22
  const { execFileSync } = require('node:child_process');
23
23
  const { verifyRun } = require('../runVerify.cjs');
24
24
 
25
+ // Sidecar goes through config.writeJsonSync, whose allow-list rejects os.tmpdir() fixtures.
26
+ let writeSpy;
27
+ beforeAll(() => {
28
+ writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
29
+ require('node:fs').writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
30
+ return { ok: true, mtimeMs: 0 };
31
+ });
32
+ });
33
+ afterAll(() => writeSpy.mockRestore());
34
+
25
35
  // ─── helpers (mirrors runVerify.test.cjs) ─────────────────────────────────────
26
36
 
27
37
  function makeTmpDir() {
@@ -19,7 +19,7 @@
19
19
  * Expected: clean → null (caller may stamp 'completed')
20
20
  */
21
21
 
22
- import { test } from 'vitest';
22
+ import { test, beforeAll, afterAll, vi } from 'vitest';
23
23
  const assert = require('node:assert/strict');
24
24
  const os = require('node:os');
25
25
  const fs = require('node:fs');
@@ -27,6 +27,17 @@ const path = require('node:path');
27
27
  const { execFileSync } = require('node:child_process');
28
28
  const { verifyRun, parseLog } = require('../runVerify.cjs');
29
29
 
30
+ // The verdicts sidecar goes through config.writeJsonSync, whose write allow-list
31
+ // rejects os.tmpdir() fixtures. Route it to a plain write for these fixtures.
32
+ let writeSpy;
33
+ beforeAll(() => {
34
+ writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
35
+ fs.writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
36
+ return { ok: true, mtimeMs: 0 };
37
+ });
38
+ });
39
+ afterAll(() => writeSpy.mockRestore());
40
+
30
41
  // ─── helpers ──────────────────────────────────────────────────────────────────
31
42
 
32
43
  /** Create a temp directory that is cleaned up after the test. */
@@ -8,7 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- import { test, expect, beforeEach, afterEach } from 'vitest';
11
+ import { test, expect, beforeEach, afterEach, vi } from 'vitest';
12
12
 
13
13
  const fs = require('node:fs');
14
14
  const os = require('node:os');
@@ -93,3 +93,114 @@ test('a growing transcript is re-summed after the file mtime/size change', async
93
93
  const after = await usageFor(cwd, [sessionId]);
94
94
  expect(after[sessionId]).toEqual({ inputTokens: 13, outputTokens: 6 });
95
95
  });
96
+
97
+ // ── incremental tail parse + bounded cache ─────────────────────────────────
98
+
99
+ const fsp = require('node:fs/promises');
100
+ const transcripts = require('../transcripts.cjs');
101
+
102
+ function fromScratch(filePath) {
103
+ let inputTokens = 0;
104
+ let outputTokens = 0;
105
+ for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
106
+ if (!line) continue;
107
+ let o;
108
+ try { o = JSON.parse(line); } catch { continue; }
109
+ const u = o.usage || {};
110
+ inputTokens += u.input_tokens ?? 0;
111
+ outputTokens += u.output_tokens ?? 0;
112
+ }
113
+ return { inputTokens, outputTokens };
114
+ }
115
+
116
+ test('incremental result equals a from-scratch parse across 5 appends (partial line + repeated id)', async () => {
117
+ const sessionId = 'sess-incr';
118
+ const filePath = writeTranscript(cwd, sessionId, [
119
+ { id: 'm1', usage: { input_tokens: 10, output_tokens: 1 } },
120
+ ]);
121
+ const check = async () => {
122
+ const r = await usageFor(cwd, [sessionId]);
123
+ expect(r[sessionId]).toEqual(fromScratch(filePath));
124
+ };
125
+ await check();
126
+ const l2 = JSON.stringify({ id: 'm2', usage: { input_tokens: 20, output_tokens: 2 } });
127
+ // 1: append ending mid-line
128
+ fs.appendFileSync(filePath, l2.slice(0, 15));
129
+ await check();
130
+ // 2: complete that line
131
+ fs.appendFileSync(filePath, l2.slice(15) + '\n');
132
+ await check();
133
+ // 3: repeat an earlier message id (full parse sums it again)
134
+ fs.appendFileSync(filePath, JSON.stringify({ id: 'm1', usage: { input_tokens: 10, output_tokens: 1 } }) + '\n');
135
+ await check();
136
+ // 4: non-usage + malformed line
137
+ fs.appendFileSync(filePath, JSON.stringify({ type: 'user' }) + '\nnot json\n');
138
+ await check();
139
+ // 5: multibyte content then usage
140
+ fs.appendFileSync(filePath, JSON.stringify({ t: 'héllo 日本', usage: { input_tokens: 4, output_tokens: 4 } }) + '\n');
141
+ await check();
142
+ expect((await usageFor(cwd, [sessionId]))[sessionId]).toEqual({ inputTokens: 44, outputTokens: 8 });
143
+ });
144
+
145
+ test('the call after a one-line append reads < 4 KB from disk', async () => {
146
+ const sessionId = 'sess-bytes';
147
+ const big = Array.from({ length: 2000 }, (_, i) => ({ i, pad: 'x'.repeat(100), usage: { input_tokens: 1, output_tokens: 1 } }));
148
+ const filePath = writeTranscript(cwd, sessionId, big);
149
+ await usageFor(cwd, [sessionId]);
150
+ expect(fs.statSync(filePath).size).toBeGreaterThan(200_000);
151
+
152
+ fs.appendFileSync(filePath, JSON.stringify({ usage: { input_tokens: 5, output_tokens: 5 } }) + '\n');
153
+ const fh = await fsp.open(filePath, 'r');
154
+ const proto = Object.getPrototypeOf(fh);
155
+ await fh.close();
156
+ const orig = proto.read;
157
+ const lengths = [];
158
+ const spy = vi.spyOn(proto, 'read').mockImplementation(function (buf, off, len, pos) {
159
+ lengths.push(len);
160
+ return orig.call(this, buf, off, len, pos);
161
+ });
162
+ try {
163
+ const r = await usageFor(cwd, [sessionId]);
164
+ expect(r[sessionId]).toEqual({ inputTokens: 2005, outputTokens: 2005 });
165
+ } finally {
166
+ spy.mockRestore();
167
+ }
168
+ expect(lengths.length).toBeGreaterThan(0);
169
+ expect(lengths.reduce((a, b) => a + b, 0)).toBeLessThan(4096);
170
+ });
171
+
172
+ test('truncation / replacement triggers a full re-parse', async () => {
173
+ const sessionId = 'sess-trunc';
174
+ const filePath = writeTranscript(cwd, sessionId, [
175
+ { usage: { input_tokens: 100, output_tokens: 100 } },
176
+ { usage: { input_tokens: 100, output_tokens: 100 } },
177
+ ]);
178
+ await usageFor(cwd, [sessionId]);
179
+ fs.writeFileSync(filePath, JSON.stringify({ usage: { input_tokens: 1, output_tokens: 2 } }) + '\n');
180
+ expect((await usageFor(cwd, [sessionId]))[sessionId]).toEqual({ inputTokens: 1, outputTokens: 2 });
181
+ });
182
+
183
+ test('usageCache is bounded and evicts the least-recently-used entry', async () => {
184
+ const cache = transcripts.__usageCacheForTest;
185
+ const max = transcripts.__usageCacheMaxForTest;
186
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-usage-lru-'));
187
+ try {
188
+ const files = [];
189
+ for (let i = 0; i < max + 5; i++) {
190
+ const f = path.join(dir, `s${i}.jsonl`);
191
+ fs.writeFileSync(f, JSON.stringify({ usage: { input_tokens: 1, output_tokens: 1 } }) + '\n');
192
+ files.push(f);
193
+ }
194
+ const before = cache.size;
195
+ for (const f of files) {
196
+ // usageForOne is reached through usageFor's path; call via a cwd-independent route.
197
+ await transcripts.__usageForOneForTest(f);
198
+ }
199
+ expect(cache.size).toBeLessThanOrEqual(max);
200
+ expect(before).toBeLessThanOrEqual(max);
201
+ expect(cache.has(files[0])).toBe(false);
202
+ expect(cache.has(files[files.length - 1])).toBe(true);
203
+ } finally {
204
+ fs.rmSync(dir, { recursive: true, force: true });
205
+ }
206
+ });
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.93.0",
3
- "gitSha": "78fa526eb94e044b2c72c736681e956bee653451",
4
- "gitShortSha": "78fa526",
2
+ "version": "0.94.0",
3
+ "gitSha": "b5777198add78466da6d34b83d818bb1352f999a",
4
+ "gitShortSha": "b577719",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-09-21T03:09:24.532Z",
6
+ "builtAt": "2026-09-21T19:59:46.626Z",
7
7
  "dirty": false
8
8
  }