claude-code-session-manager 0.93.0 → 0.95.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 (78) hide show
  1. package/dist/assets/{DataModel-DDGx3qrn.js → DataModel-DZK9QZpV.js} +1 -1
  2. package/dist/assets/{History-CHBjYZyD.js → History-BDyJUGtR.js} +2 -2
  3. package/dist/assets/{Hooks-DG4c-Rzz.js → Hooks-CmK6l0uf.js} +3 -3
  4. package/dist/assets/HostBilko-D8TIx-Bx.js +1 -0
  5. package/dist/assets/{Library-CN1h5rAE.js → Library-DTxpOjx8.js} +25 -25
  6. package/dist/assets/{MarkdownEditor-DmBxrhdn.js → MarkdownEditor-BPJyrevq.js} +1 -1
  7. package/dist/assets/McpServers-B_rvIYDs.js +2 -0
  8. package/dist/assets/{Memory-Dk4uDCjg.js → Memory-B-9GlHqe.js} +6 -6
  9. package/dist/assets/{Permissions-BovobeZw.js → Permissions-DhMMp27n.js} +3 -3
  10. package/dist/assets/{Plugins-DvOf_B62.js → Plugins-DKfAlmMY.js} +2 -2
  11. package/dist/assets/{ProvenanceBadge-Dutz_4OY.js → ProvenanceBadge-ptlItdCK.js} +1 -1
  12. package/dist/assets/{SaveBar-CT5u6ZCJ.js → SaveBar-CSbNSl2W.js} +1 -1
  13. package/dist/assets/Scheduler--rn1QBYc.js +16 -0
  14. package/dist/assets/{ScopeSwitcher-BUYDpz2v.js → ScopeSwitcher-BJof-GWH.js} +1 -1
  15. package/dist/assets/{Settings-DIRmr3tL.js → Settings-Dsx6XZP5.js} +3 -3
  16. package/dist/assets/{SkillReferenceGraph-CdGr6d3_.js → SkillReferenceGraph-CGl4B89-.js} +2 -2
  17. package/dist/assets/{Skills-BQOheQv4.js → Skills-D522Zfol.js} +2 -2
  18. package/dist/assets/{SystemPrompt-5LsimKbX.js → SystemPrompt-DKj-q39J.js} +1 -1
  19. package/dist/assets/{TagLibrary-DreLsT3n.js → TagLibrary-BH7CG7-6.js} +1 -1
  20. package/dist/assets/{TiptapBody-C9kmlBnI.js → TiptapBody-BbcJL9az.js} +1 -1
  21. package/dist/assets/{Toggle-BKLCBu9A.js → Toggle-DAslvnnv.js} +1 -1
  22. package/dist/assets/{index-Db_SK9rW.js → index-Yf8e7VCB.js} +821 -821
  23. package/dist/assets/{settingsSchema-B5jBhSUP.js → settingsSchema-pzp0TgvN.js} +1 -1
  24. package/dist/index.html +1 -1
  25. package/package.json +8 -3
  26. package/scripts/README.md +6 -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__/health-tick-liveness.test.cjs +12 -0
  31. package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
  32. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
  33. package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
  34. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
  35. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
  36. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
  37. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
  38. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
  39. package/src/main/__tests__/runVerify.test.cjs +12 -1
  40. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +15 -0
  41. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +89 -0
  42. package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
  43. package/src/main/build-info.json +4 -4
  44. package/src/main/chatRunner.cjs +45 -10
  45. package/src/main/crashDiagnostics.cjs +9 -0
  46. package/src/main/git.cjs +9 -2
  47. package/src/main/health.cjs +25 -1
  48. package/src/main/historyAggregator.cjs +2 -19
  49. package/src/main/index.cjs +7 -8
  50. package/src/main/ipcSchemas.cjs +13 -3
  51. package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
  52. package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
  53. package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
  54. package/src/main/lib/__tests__/loopDelay.test.cjs +68 -0
  55. package/src/main/lib/__tests__/usageCircuit.test.cjs +86 -17
  56. package/src/main/lib/agentPersonaSchema.cjs +2 -0
  57. package/src/main/lib/childWithLog.cjs +92 -51
  58. package/src/main/lib/delegationReadiness.cjs +3 -1
  59. package/src/main/lib/epicSpawnPlan.cjs +27 -8
  60. package/src/main/lib/epicTranscriptPath.cjs +5 -1
  61. package/src/main/lib/headTailBuffer.cjs +43 -0
  62. package/src/main/lib/intradayRefresh.cjs +33 -0
  63. package/src/main/lib/loopDelay.cjs +44 -0
  64. package/src/main/lib/lruCache.cjs +39 -0
  65. package/src/main/lib/openExternalApp.cjs +27 -9
  66. package/src/main/lib/opsErrorLog.cjs +22 -0
  67. package/src/main/lib/promptSessionSchema.cjs +2 -0
  68. package/src/main/lib/rendererRecovery.cjs +141 -0
  69. package/src/main/lib/scheduleJobSchema.cjs +3 -0
  70. package/src/main/lib/usageCircuit.cjs +40 -12
  71. package/src/main/runVerify.cjs +3 -1
  72. package/src/main/scheduler.cjs +409 -260
  73. package/src/main/transcripts.cjs +54 -11
  74. package/src/main/usage.cjs +9 -1
  75. package/src/preload/api.d.ts +20 -3
  76. package/dist/assets/HostBilko-DHXcpV2c.js +0 -1
  77. package/dist/assets/McpServers-Beb_HD6I.js +0 -2
  78. package/dist/assets/Scheduler-B7GSnZj5.js +0 -16
@@ -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-Yf8e7VCB.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-Yf8e7VCB.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.95.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,12 +33,14 @@
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",
40
40
  "prepublishOnly": "node scripts/write-build-info.cjs && vite build",
41
41
  "test:unit": "vitest run",
42
+ "bench": "node scripts/bench/main-bench.cjs",
43
+ "bench:renderer": "vitest run --config vitest.bench.config.ts",
42
44
  "test:e2e": "xvfb-run -a playwright test",
43
45
  "smoke:darwin": "playwright test tests/smoke/darwin-boot.spec.ts",
44
46
  "test:e2e:mic": "xvfb-run -a playwright test e2e/mic.spec.mjs",
@@ -53,7 +55,10 @@
53
55
  "lint:unregistered-tests": "node scripts/check-unregistered-tests.cjs",
54
56
  "lint:docs": "node scripts/check-doc-hierarchy.cjs",
55
57
  "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",
58
+ "lint:any": "node scripts/check-explicit-any.cjs",
59
+ "lint:main-ts-check": "node scripts/check-main-ts-check.cjs",
60
+ "lint:filenames": "node scripts/check-tracked-filenames.cjs",
61
+ "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
62
  "sync:settings-schema": "node scripts/sync-settings-schema.cjs"
58
63
  },
59
64
  "keywords": [
package/scripts/README.md CHANGED
@@ -9,9 +9,13 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
9
9
  | script | invoked by | shipped on npm |
10
10
  | --- | --- | --- |
11
11
  | audit-ops-hygiene.cjs | manual; named in `ops-sweep` + `develop` SKILL.md | yes |
12
+ | bench/main-bench.cjs | `npm run bench` (main-process micro-benches, plain Node); renderer half is `npm run bench:renderer` (`tests/bench/`, `vitest.bench.config.ts`, not in `test:unit`) | no |
12
13
  | check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
13
14
  | check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
14
15
  | check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
16
+ | 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 |
17
+ | 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 |
18
+ | 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
19
  | check-unregistered-tests.cjs | `npm run lint:unregistered-tests` (in `lint`) | no |
16
20
  | check-unstable-selectors.cjs | `npm run lint:selectors` (in `lint`) | no |
17
21
  | cleanup-nested-queue-stubs.cjs | manual one-shot; dispatches to cleanup-worktree-ops-stubs.cjs | no |
@@ -51,6 +55,8 @@ The four `hooks/guard-*.cjs` are adopted by reference, not copied: `src/main/lib
51
55
 
52
56
  `cleanup-nested-queue-stubs.cjs` removes nested queue shards and then dispatches to `cleanup-worktree-ops-stubs.cjs` (its `main()`), which handles the worktree-side stubs.
53
57
 
58
+ Bench numbers are machine-relative — compare runs on the same machine only; there are no pass/fail thresholds.
59
+
54
60
  ## npm shipping
55
61
 
56
62
  `ops-sweep.cjs` and `audit-ops-hygiene.cjs` ship: they need only Node built-ins and `src/main/` (also packed), no dev-only deps. `package.json` `files` lists every shipped script individually; `__tests__/package-files.test.cjs` asserts it.
@@ -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));
@@ -92,6 +92,18 @@ test('no false positive: billing utilization at/above utilizationThreshold (when
92
92
  assert.strictEqual(result.reason, 'utilization-at-threshold');
93
93
  });
94
94
 
95
+ test('utilization hold with reset days out is flagged longHold; within 5h is benign', () => {
96
+ const far = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, utilizationWindow: 'weekly_all', nextReset: new Date(NOW + 3 * 86_400_000).toISOString() }, NOW);
97
+ assert.strictEqual(far.stalled, false);
98
+ assert.strictEqual(far.longHold, true);
99
+ assert.strictEqual(far.window, 'weekly_all');
100
+ const unknown = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, nextReset: null }, NOW);
101
+ assert.strictEqual(unknown.longHold, true);
102
+ const near = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, utilizationWindow: 'five_hour', nextReset: new Date(NOW + 2 * 3_600_000).toISOString() }, NOW);
103
+ assert.strictEqual(near.stalled, false);
104
+ assert.strictEqual(near.longHold, undefined);
105
+ });
106
+
95
107
  test('no false positive: no pending jobs at all', () => {
96
108
  const q = baseQueue({ jobs: [] });
97
109
  const result = evaluateTickLiveness(q, null, NOW);
@@ -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. */
@@ -31,6 +31,7 @@ const {
31
31
  billingResetForPause,
32
32
  computeEffectiveResumeAt,
33
33
  computeResumeDelay,
34
+ computeDegradedBudget,
34
35
  } = require('../scheduler.cjs');
35
36
  const billing = require('../usage.cjs');
36
37
 
@@ -135,3 +136,17 @@ test('computeResumeDelay: a reset far enough out DOES trip the overflow guard',
135
136
  const { tooFar } = computeResumeDelay(thirtyDaysOut, now);
136
137
  assert.strictEqual(tooFar, true);
137
138
  });
139
+
140
+ test('refreshNextReset (via billingResetForPause) seeds lastGoodUsagePayload: a following degraded cycle yields the fetched percent, not 100', async () => {
141
+ const originalFetchUsage = billing.fetchUsage;
142
+ billing.fetchUsage = async () => ({
143
+ kind: 'ok',
144
+ data: { usage: { limits: [{ kind: 'weekly_all', percent: 64, resets_at: '2026-09-24T17:00:00Z', scope: null, is_active: true }] } },
145
+ });
146
+ try {
147
+ await billingResetForPause();
148
+ assert.strictEqual(computeDegradedBudget().utilization, 64);
149
+ } finally {
150
+ billing.fetchUsage = originalFetchUsage;
151
+ }
152
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * scheduler-utilization-hold.test.cjs — the when-available gate holds on the
3
+ * BINDING window and records a machine-readable hold (window/percent/
4
+ * threshold/reset) on the snapshot; under threshold it dispatches.
5
+ * Run: timeout 180 npx vitest run src/main/__tests__/scheduler-utilization-hold.test.cjs
6
+ */
7
+ 'use strict';
8
+
9
+ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ let tmpHome;
15
+ let originalHome;
16
+ let scheduler;
17
+ let billing;
18
+ let originalFetchUsage;
19
+
20
+ beforeAll(() => {
21
+ originalHome = process.env.HOME;
22
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-utilhold-'));
23
+ process.env.HOME = tmpHome;
24
+ fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
25
+ fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
26
+ scheduler = require('../scheduler.cjs');
27
+ billing = require('../usage.cjs');
28
+ originalFetchUsage = billing.fetchUsage;
29
+ });
30
+
31
+ afterAll(() => {
32
+ billing.fetchUsage = originalFetchUsage;
33
+ process.env.HOME = originalHome;
34
+ fs.rmSync(tmpHome, { recursive: true, force: true });
35
+ });
36
+
37
+ afterEach(() => { billing.fetchUsage = originalFetchUsage; });
38
+
39
+ const RESET = '2026-09-24T17:00:00Z';
40
+ function usageWith(weeklyPct) {
41
+ return {
42
+ kind: 'ok',
43
+ data: {
44
+ usage: {
45
+ five_hour: { utilization: 10, resets_at: '2026-09-21T20:00:00Z' },
46
+ limits: [
47
+ { kind: 'session', percent: 10, scope: null, resets_at: '2026-09-21T20:00:00Z' },
48
+ { kind: 'weekly_all', percent: weeklyPct, scope: null, resets_at: RESET },
49
+ ],
50
+ },
51
+ },
52
+ };
53
+ }
54
+
55
+ // dependsOn never resolves so a tick reaches the picker (stamping
56
+ // lastDispatchAttemptAt) without ever spawning a real job.
57
+ function state() {
58
+ return {
59
+ config: { firePolicy: 'when-available', utilizationThreshold: 90 },
60
+ paused: null,
61
+ jobs: [{ slug: '9002-hold', title: 'x', cwd: tmpHome, status: 'pending', dependsOn: ['never'] }],
62
+ };
63
+ }
64
+
65
+ test('holds (no tick) and records the hold when the binding weekly window is over threshold', async () => {
66
+ billing.fetchUsage = async () => usageWith(91);
67
+ await scheduler.refreshNextReset();
68
+ const logs = [];
69
+ const orig = console.log;
70
+ console.log = (...a) => { logs.push(a.join(' ')); };
71
+ try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
72
+ const payload = scheduler.buildScheduleStatePayload(state());
73
+ expect(payload.utilizationHold).toEqual({ window: 'weekly_all', percent: 91, threshold: 90, resetsAt: RESET });
74
+ expect(payload.utilizationWindow).toBe('weekly_all');
75
+ expect(logs.some((l) => l.includes('utilization-held') && l.includes('weekly_all'))).toBe(true);
76
+ expect(logs.some((l) => l.includes('— ticking'))).toBe(false);
77
+ });
78
+
79
+ test('ticks and clears the hold when the binding window is under threshold (weekly_all 64)', async () => {
80
+ billing.fetchUsage = async () => usageWith(64);
81
+ await scheduler.refreshNextReset();
82
+ const logs = [];
83
+ const orig = console.log;
84
+ console.log = (...a) => { logs.push(a.join(' ')); };
85
+ try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
86
+ await scheduler.tickQueue();
87
+ expect(scheduler.buildScheduleStatePayload(state()).utilizationHold).toBeNull();
88
+ expect(logs.some((l) => l.includes('— ticking'))).toBe(true);
89
+ });