borgmcp 1.0.49 → 1.0.51

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.
@@ -1,48 +1,48 @@
1
- import{dirname as ne,basename as C}from"node:path";import{randomUUID as oe}from"node:crypto";import{roleSlug as ie,matchRoleByName as ae,pickDefaultRole as le}from"./role-resolver.js";import{deriveCubeName as se,parseGitRemote as ce,sanitizeRemoteUrl as ue}from"./cube-name.js";import{validateName as K}from"./name-validator.js";import{renderAssimilationWelcome as me}from"./assimilate-welcome.js";import{shellEscape as fe}from"./shell-escape.js";import{withCodexCwdArg as de}from"./codex-remote.js";import{buildAgentKickoffPrompt as he,buildKickoffWakePathClause as ge,recordCodexWakeTarget as we,socketPathFromRemoteArgs as be}from"./codex-launch.js";import{perWorktreeBranchName as O,adoptWorktree as ke,computeWorktreePath as q,localBranchExists as z,isMerged as ye}from"./worktree-lifecycle.js";import{DroneEvictedError as ve}from"./drone-lifecycle.js";import{codexBorgSessionConfigArgs as $e}from"./launch-gate.js";import{resolveLaunchEnv as pe,resolveOllamaBaseUrl as xe,parseModel as Se}from"./model-presets.js";async function Ke(r,e){if(r.role!==void 0){const t=K(r.role);if(!t.ok)return e.stderr(t.error+`
2
- `),1}if(r.flags.worktree!==void 0){const t=K(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
- `),1}let a=await e.getCachedAuth();if(!a){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;a=await e.runSetup()}const n=e.findProjectRoot(e.cwd());let o;if(r.flags.cubeName)o=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],n),i=t.status===0?t.stdout:null;if(o=se(n,i),i){const u=ue(i),m=u?ce(u):null;u&&!m&&o&&e.stderr(`couldn't parse git remote '${u}' \u2014 using directory name '${o}' as cube name
4
- `)}}let l=null;if(o&&o.includes("@")&&o.includes(":")){const t=o.lastIndexOf(":");l={ownerEmail:o.substring(0,t),cubeName:o.substring(t+1)},o=l.cubeName}const R=e.cwd();e.stderr(`Checking your cubes\u2026
5
- `);let A;try{A=await e.listCubes(a.apiUrl,a.token)}catch(t){const i=t instanceof Error?t.message:String(t);if(i.includes("Authentication required")||i.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
- `),a=await e.runSetup(),A=await e.listCubes(a.apiUrl,a.token);else throw t}const N=A.find(t=>t.name===o);if(!N&&l)return e.stderr(`No cube named '${l.cubeName}' accessible to you owned by '${l.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
- `),1;let s,P;if(N)s=await e.getCube(a.apiUrl,a.token,N.id),P=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const i=await e.listTemplates(a.apiUrl,a.token),u=["First drone joining a new cube. Apply a template?"];i.forEach(($,p)=>{const _=p===0?" (default)":"";u.push(` ${p+1}) ${$.name}${_} \u2014 ${$.description}`)}),u.push(` ${i.length+1}) skip \u2014 no template`);const m=(await e.prompt(u.join(`
1
+ import{dirname as K,basename as C}from"node:path";import{randomUUID as oe}from"node:crypto";import{roleSlug as ie,matchRoleByName as ae,pickDefaultRole as le}from"./role-resolver.js";import{deriveCubeName as se,parseGitRemote as ce,sanitizeRemoteUrl as ue}from"./cube-name.js";import{validateName as q}from"./name-validator.js";import{renderAssimilationWelcome as me}from"./assimilate-welcome.js";import{shellEscape as de}from"./shell-escape.js";import{withCodexCwdArg as fe}from"./codex-remote.js";import{buildAgentKickoffPrompt as he,buildKickoffWakePathClause as ge,recordCodexWakeTarget as we,socketPathFromRemoteArgs as be}from"./codex-launch.js";import{perWorktreeBranchName as W,adoptWorktree as ke,computeWorktreePath as z,localBranchExists as J,isMerged as ye}from"./worktree-lifecycle.js";import{DroneEvictedError as ve}from"./drone-lifecycle.js";import{codexBorgSessionConfigArgs as $e}from"./launch-gate.js";import{inboxPathForDrone as pe}from"./cubes.js";import{resolveLaunchEnv as xe,resolveOllamaBaseUrl as Se,parseModel as Ce}from"./model-presets.js";import{unlinkSync as Re}from"node:fs";import{gcOrphanInboxesForCube as Ee,defaultListInboxLogs as _e,defaultInboxLivenessDeps as Ae,isInboxLive as Ie,ORPHAN_INBOX_STALE_MS as Ne}from"./gc-orphan-inboxes.js";async function rt(r,e){if(r.role!==void 0){const t=q(r.role);if(!t.ok)return e.stderr(t.error+`
2
+ `),1}if(r.flags.worktree!==void 0){const t=q(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
+ `),1}let a=await e.getCachedAuth();if(!a){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;a=await e.runSetup()}const o=e.findProjectRoot(e.cwd());let i;if(r.flags.cubeName)i=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],o),n=t.status===0?t.stdout:null;if(i=se(o,n),n){const c=ue(n),m=c?ce(c):null;c&&!m&&i&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${i}' as cube name
4
+ `)}}let l=null;if(i&&i.includes("@")&&i.includes(":")){const t=i.lastIndexOf(":");l={ownerEmail:i.substring(0,t),cubeName:i.substring(t+1)},i=l.cubeName}const R=e.cwd();e.stderr(`Checking your cubes\u2026
5
+ `);let A;try{A=await e.listCubes(a.apiUrl,a.token)}catch(t){const n=t instanceof Error?t.message:String(t);if(n.includes("Authentication required")||n.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
+ `),a=await e.runSetup(),A=await e.listCubes(a.apiUrl,a.token);else throw t}const I=A.find(t=>t.name===i);if(!I&&l)return e.stderr(`No cube named '${l.cubeName}' accessible to you owned by '${l.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
+ `),1;let u,N;if(I)u=await e.getCube(a.apiUrl,a.token,I.id),N=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const n=await e.listTemplates(a.apiUrl,a.token),c=["First drone joining a new cube. Apply a template?"];n.forEach(($,p)=>{const _=p===0?" (default)":"";c.push(` ${p+1}) ${$.name}${_} \u2014 ${$.description}`)}),c.push(` ${n.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
8
8
  `)+`
9
- [1]: `)).trim(),y=m===""?1:parseInt(m,10);if(Number.isNaN(y)||y<1||y>i.length+1)return e.stderr(`invalid choice "${m}"
10
- `),1;t=y<=i.length?i[y-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
9
+ [1]: `)).trim(),y=m===""?1:parseInt(m,10);if(Number.isNaN(y)||y<1||y>n.length+1)return e.stderr(`invalid choice "${m}"
10
+ `),1;t=y<=n.length?n[y-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
11
11
  Pass --template <name>, --no-template, or --yes (defaults to starter).
12
- `),1;t="starter"}e.stderr(o?`Creating cube '${o}'\u2026
12
+ `),1;t="starter"}e.stderr(i?`Creating cube '${i}'\u2026
13
13
  `:`Creating your cube\u2026
14
- `),s=await e.createCube(a.apiUrl,a.token,t?{name:o??void 0,template:t}:{name:o??void 0}),P=!0}let d;if(r.role!==void 0){if(d=ae(s.roles,r.role),!d){const t=s.roles.map(m=>m.name).join(", "),i=_e(r.role,s.roles.map(m=>m.name)),u=i?` Did you mean "${i}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${s.name}". Available: ${t}.${u}
14
+ `),u=await e.createCube(a.apiUrl,a.token,t?{name:i??void 0,template:t}:{name:i??void 0}),N=!0}let f;if(r.role!==void 0){if(f=ae(u.roles,r.role),!f){const t=u.roles.map(m=>m.name).join(", "),n=De(r.role,u.roles.map(m=>m.name)),c=n?` Did you mean "${n}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${u.name}". Available: ${t}.${c}
15
15
  (Use --template <name> on first-drone setup or run \`borg_create-role\` from inside Claude.)
16
- `),1}}else if(d=le(s.roles,{isFirstDrone:P}),!d)return e.stderr(`cube "${s.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg_create-role\` from inside Claude to set up roles.
17
- `),1;const x=await e.getActiveCube();let S;if(x&&r.flags.here)if(x.cubeId===s.id)S=x.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
18
- `),1;const H=r.flags.worktree!==void 0||x!==null&&!r.flags.here,V=S??x?.droneId??null,T=H?null:await e.getLaunchModel(s.id,n,V),b=r.flags.model??T?.model??d.default_model??null,I=xe(process.env,b!=null&&b===T?.model?T?.ollamaBaseUrl:void 0);if(b){const t=await e.checkModelReachable(b,e.fetch,I);if(!t.ok)return e.stderr(`${t.message}
19
- `),1}const h=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${s.name}' as ${d.name}\u2026
20
- `);let c;try{c=await e.assimilate(a.apiUrl,a.token,{cube_id:s.id,role_id:d.id,hostname:e.getHostname(),agent_kind:h,model:b,...S?{prior_drone_id:S}:{}})}catch(t){if(t instanceof ve&&S!=null)return e.stderr(`seat evicted \u2014 this worktree's saved seat was evicted from the cube. Re-assimilate fresh from a terminal, or remove this worktree.
21
- `),1;const i=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${i}
22
- `),1}const k=s.roles.find(t=>t.id===c.role_id)??d;c.reattached?e.stderr(`re-attached to existing seat ${c.drone_label} (session token rotated, no new drone minted)
23
- `):k.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${k.name}" instead.
24
- `);const Q=H;let g=null;if(Q){const t=e.runSync("git",["rev-parse","--verify","HEAD"],n);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
16
+ `),1}}else if(f=le(u.roles,{isFirstDrone:N}),!f)return e.stderr(`cube "${u.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg_create-role\` from inside Claude to set up roles.
17
+ `),1;const x=await e.getActiveCube();let S;if(x&&r.flags.here)if(x.cubeId===u.id)S=x.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
18
+ `),1;const M=r.flags.worktree!==void 0||x!==null&&!r.flags.here,X=S??x?.droneId??null,P=M?null:await e.getLaunchModel(u.id,o,X),b=r.flags.model??P?.model??f.default_model??null,L=Se(process.env,b!=null&&b===P?.model?P?.ollamaBaseUrl:void 0);if(b){const t=await e.checkModelReachable(b,e.fetch,L);if(!t.ok)return e.stderr(`${t.message}
19
+ `),1}const h=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${u.name}' as ${f.name}\u2026
20
+ `);let s;try{s=await e.assimilate(a.apiUrl,a.token,{cube_id:u.id,role_id:f.id,hostname:e.getHostname(),agent_kind:h,model:b,...S?{prior_drone_id:S}:{}})}catch(t){if(t instanceof ve&&S!=null)return e.stderr(`seat evicted \u2014 this worktree's saved seat was evicted from the cube. Re-assimilate fresh from a terminal, or remove this worktree.
21
+ `),1;const n=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${n}
22
+ `),1}const k=u.roles.find(t=>t.id===s.role_id)??f;s.reattached?e.stderr(`re-attached to existing seat ${s.drone_label} (session token rotated, no new drone minted)
23
+ `):k.id!==f.id&&e.stderr(`Note: your invite didn't grant the "${f.name}" role \u2014 assimilated as "${k.name}" instead.
24
+ `);const Q=M;let g=null;if(Q){const t=e.runSync("git",["rev-parse","--verify","HEAD"],o);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
25
25
  Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
26
26
  OR: pass --here to skip the sibling spawn and use the current directory
27
- `),1;e.runSync("git",["fetch","origin"],n);let i="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],n).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],n).status===0&&(i="origin/master");const m=t.stdout.trim(),y=e.runSync("git",["rev-parse",i],n).stdout.trim();m!==y&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${i} (${y.slice(0,7)}); new worktree will start on ${i}
28
- `);const $=C(n),p=r.flags.worktree??ie(k.name);if(p.length===0)return e.stderr(`cannot derive a worktree name from role "${k.name}"; pass an explicit --worktree <name>
29
- `),1;const _=e.homedir();let f=q(_,$,p),v=O(C(f),$),Y=2;for(;e.pathExists(f)||Ee(e,n,f)||z(e.runSync,n,v)&&!ye(e.runSync,n,v,i);)f=q(_,$,p,Y),v=O(C(f),$),Y++;e.mkdirp(ne(f));const G=z(e.runSync,n,v)?e.runSync("git",["worktree","add",f,v],n):e.runSync("git",["worktree","add","-b",v,f,i],n);if(G.status!==0)return e.stderr(`git worktree add failed: ${J(G.stderr)}
30
- `),1;e.stderr(`spawned sibling worktree at ${f} on branch ${v} (${i}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
31
- `),e.chdir(f),e.stderr(Ce(f,v,n)),g=e.cwd()}try{await e.setActiveCube({cubeId:c.cube_id,droneId:c.drone_id,name:s.name,sessionToken:c.session_token,droneLabel:c.drone_label,apiUrl:a.apiUrl,roleName:k.name,isHumanSeat:k.is_human_seat,...k.role_class?{roleClass:k.role_class}:{}})}catch(t){const i=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${i}
32
- `),g){const u=e.runSync("git",["worktree","remove","--force",g],n);u.status===0?e.stderr(`rolled back spawned worktree at ${g}
33
- `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${g}\` (rollback attempt failed: ${J(u.stderr).trim()||"unknown"})
34
- `)}return 1}e.setTerminalTitle(c.drone_label,s.name);const X=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(me(k.name,s.name,X));const w=e.cwd();try{e.installProjectSessionHook(w)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${w}; it will be re-attempted on the next borg launch
35
- `)}if(!g){e.runSync("git",["fetch","origin","--prune"],w);const t=O(C(w),C(n)),i=ke(e.runSync,w,t,"origin/main");i.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
36
- `),e.stderr(Re(w,t))):i.message&&e.stderr(`worktree sync: ${i.message}
27
+ `),1;e.runSync("git",["fetch","origin"],o);let n="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],o).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],o).status===0&&(n="origin/master");const m=t.stdout.trim(),y=e.runSync("git",["rev-parse",n],o).stdout.trim();m!==y&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${n} (${y.slice(0,7)}); new worktree will start on ${n}
28
+ `);const $=C(o),p=r.flags.worktree??ie(k.name);if(p.length===0)return e.stderr(`cannot derive a worktree name from role "${k.name}"; pass an explicit --worktree <name>
29
+ `),1;const _=e.homedir();let d=z(_,$,p),v=W(C(d),$),Y=2;for(;e.pathExists(d)||Te(e,o,d)||J(e.runSync,o,v)&&!ye(e.runSync,o,v,n);)d=z(_,$,p,Y),v=W(C(d),$),Y++;e.mkdirp(K(d));const G=J(e.runSync,o,v)?e.runSync("git",["worktree","add",d,v],o):e.runSync("git",["worktree","add","-b",v,d,n],o);if(G.status!==0)return e.stderr(`git worktree add failed: ${V(G.stderr)}
30
+ `),1;e.stderr(`spawned sibling worktree at ${d} on branch ${v} (${n}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
31
+ `),e.chdir(d),e.stderr(Pe(d,v,o)),g=e.cwd()}try{await e.setActiveCube({cubeId:s.cube_id,droneId:s.drone_id,name:u.name,sessionToken:s.session_token,droneLabel:s.drone_label,apiUrl:a.apiUrl,roleName:k.name,isHumanSeat:k.is_human_seat,...k.role_class?{roleClass:k.role_class}:{}})}catch(t){const n=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${n}
32
+ `),g){const c=e.runSync("git",["worktree","remove","--force",g],o);c.status===0?e.stderr(`rolled back spawned worktree at ${g}
33
+ `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${g}\` (rollback attempt failed: ${V(c.stderr).trim()||"unknown"})
34
+ `)}return 1}try{const t=Ae(),n=K(pe(s.cube_id,s.drone_id));Ee({cubeInboxDir:n,selfDroneId:s.drone_id,deps:{listInboxLogs:_e,isLive:c=>Ie(c,t),droneState:()=>"absent",unlink:c=>Re(c),now:t.now,staleMs:Ne}})}catch{}e.setTerminalTitle(s.drone_label,u.name);const Z=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(me(k.name,u.name,Z));const w=e.cwd();try{e.installProjectSessionHook(w)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${w}; it will be re-attempted on the next borg launch
35
+ `)}if(!g){e.runSync("git",["fetch","origin","--prune"],w);const t=W(C(w),C(o)),n=ke(e.runSync,w,t,"origin/main");n.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
36
+ `),e.stderr(Le(w,t))):n.message&&e.stderr(`worktree sync: ${n.message}
37
37
  `)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${h} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
38
- `);const Z=e.getInboxPath(c.cube_id,c.drone_id),U=h==="codex"?`borg-wake-${oe()}`:null,ee=ge(h==="codex"?"codex":"claude",h==="claude"?Z:null);let D,M=[],E,L=null,j=null;const B=e.findProjectRoot(w);b?await e.setLaunchModel(c.cube_id,B,{model:b,ollamaBaseUrl:Se(b).kind==="ollama"?I:null}):await e.clearLaunchModel(c.cube_id,B);const F=pe(b,I),W={...process.env,...F.set,BORG_SESSION:"1"};for(const t of F.unset)delete W[t];if(h==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
39
- `),D="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg_regen manually whenever you return, and expect only fallback wakeups until relaunch."):D="Codex wake-path capability check passed: remote-control socket established for this session.",M=t.args,Object.keys(t.env).length>0&&Object.assign(W,t.env),L=be(t.args),j=t.server?.cleanup??null}E=[he({cli:h,codexWakeNonce:U,monitorClause:ee,codexWakePathClause:D})],h==="codex"&&(E=[...$e(),...M,...de(E,w)]);const te=e.exec(h,E,w,W);h==="codex"&&L&&U&&we({deps:e,cubeId:c.cube_id,droneId:c.drone_id,socketPath:L,cwd:w,previewNeedle:U,launchedAtSeconds:Math.floor(Date.now()/1e3)});const re=await te;if(j)try{j()}catch{}return g&&R!==g&&e.stderr(`
38
+ `);const ee=e.getInboxPath(s.cube_id,s.drone_id),T=h==="codex"?`borg-wake-${oe()}`:null,te=ge(h==="codex"?"codex":"claude",h==="claude"?ee:null);let D,H=[],E,U=null,O=null;const B=e.findProjectRoot(w);b?await e.setLaunchModel(s.cube_id,B,{model:b,ollamaBaseUrl:Ce(b).kind==="ollama"?L:null}):await e.clearLaunchModel(s.cube_id,B);const F=xe(b,L),j={...process.env,...F.set,BORG_SESSION:"1"};for(const t of F.unset)delete j[t];if(h==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
39
+ `),D="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg_regen manually whenever you return, and expect only fallback wakeups until relaunch."):D="Codex wake-path capability check passed: remote-control socket established for this session.",H=t.args,Object.keys(t.env).length>0&&Object.assign(j,t.env),U=be(t.args),O=t.server?.cleanup??null}E=[he({cli:h,codexWakeNonce:T,monitorClause:te,codexWakePathClause:D})],h==="codex"&&(E=[...$e(),...H,...fe(E,w)]);const re=e.exec(h,E,w,j);h==="codex"&&U&&T&&we({deps:e,cubeId:s.cube_id,droneId:s.drone_id,socketPath:U,cwd:w,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const ne=await re;if(O)try{O()}catch{}return g&&R!==g&&e.stderr(`
40
40
  Agent exited. You were working in ${g}; your shell is back in ${R}.
41
41
  To return:
42
- cd ${fe(g)}
43
- `),re}function Ce(r,e,a){return`
42
+ cd ${de(g)}
43
+ `),ne}function Pe(r,e,a){return`
44
44
  WORKTREE STEERING: You are in worktree ${r} on branch ${e}. Do ALL work HERE \u2014 cut your feature branch (fix/.../feat/...) off ${e} in THIS worktree, use relative paths / your cwd. NEVER \`git -C ${a}\` or operate on the primary checkout ${a}: the same branch can't be checked out in two worktrees, so work created in the primary won't reach your wt-branch without manual surgery (cherry-pick/merge).
45
- `}function Re(r,e){return`
45
+ `}function Le(r,e){return`
46
46
  WORKTREE STEERING: This checkout is now on branch ${e}. Do ALL work HERE in ${r} \u2014 cut your feature branch (fix/.../feat/...) off ${e}, use relative paths / your cwd.
47
- `}function J(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function Ee(r,e,a){const n=r.runSync("git",["worktree","list","--porcelain"],e);return n.status!==0?!1:n.stdout.split(`
48
- `).some(o=>o===`worktree ${a}`)}function _e(r,e){if(e.length===0)return null;const a=r.toLowerCase();let n=null;for(const o of e){const l=Ae(a,o.toLowerCase());l<=2&&(n===null||l<n.distance)&&(n={name:o,distance:l})}return n?n.name:null}function Ae(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const a=new Array(e.length+1),n=new Array(e.length+1);for(let o=0;o<=e.length;o++)a[o]=o;for(let o=1;o<=r.length;o++){n[0]=o;for(let l=1;l<=e.length;l++){const R=r[o-1]===e[l-1]?0:1;n[l]=Math.min(n[l-1]+1,a[l]+1,a[l-1]+R)}for(let l=0;l<=e.length;l++)a[l]=n[l]}return a[e.length]}export{Ke as runAssimilate,J as safeStderr,_e as suggestRoleName};
47
+ `}function V(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function Te(r,e,a){const o=r.runSync("git",["worktree","list","--porcelain"],e);return o.status!==0?!1:o.stdout.split(`
48
+ `).some(i=>i===`worktree ${a}`)}function De(r,e){if(e.length===0)return null;const a=r.toLowerCase();let o=null;for(const i of e){const l=Ue(a,i.toLowerCase());l<=2&&(o===null||l<o.distance)&&(o={name:i,distance:l})}return o?o.name:null}function Ue(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const a=new Array(e.length+1),o=new Array(e.length+1);for(let i=0;i<=e.length;i++)a[i]=i;for(let i=1;i<=r.length;i++){o[0]=i;for(let l=1;l<=e.length;l++){const R=r[i-1]===e[l-1]?0:1;o[l]=Math.min(o[l-1]+1,a[l]+1,a[l-1]+R)}for(let l=0;l<=e.length;l++)a[l]=o[l]}return a[e.length]}export{rt as runAssimilate,V as safeStderr,De as suggestRoleName};
@@ -0,0 +1,77 @@
1
+ /** §8.2 staleness threshold — ≥30 days; conservative, well beyond any plausible offline period. */
2
+ export declare const ORPHAN_INBOX_STALE_MS: number;
3
+ /** Roster signal for a drone_id. `absent` is the safe default when no roster is available. */
4
+ export type DroneRosterState = 'present' | 'evicted' | 'absent';
5
+ export interface OrphanInboxEntry {
6
+ /** the drone_id parsed from the `<drone_id>.log` filename */
7
+ droneId: string;
8
+ /** absolute path to the `.log` */
9
+ inboxPath: string;
10
+ /** local mtime of the `.log`, in ms */
11
+ mtimeMs: number;
12
+ }
13
+ export interface SelectOrphanInboxesArgs {
14
+ entries: OrphanInboxEntry[];
15
+ /** §2 HARD gate: true if ANY live-holder signal fires (pgrep / fresh-heartbeat / live-pid). */
16
+ isLive: (inboxPath: string) => boolean;
17
+ /** roster bonus (when available): a `present` member is never reaped. */
18
+ droneState: (droneId: string) => DroneRosterState;
19
+ now: number;
20
+ staleMs: number;
21
+ }
22
+ /**
23
+ * Pure, FS-free selection (mirrors the `acquireInboxLock` dep-injection style so
24
+ * the live-safety + staleness logic is unit-pinned without touching the FS).
25
+ *
26
+ * An inbox is GC-eligible ONLY when ALL hold:
27
+ * §2 NO live holder — `isLive` false (the absolute gate; one live signal vetoes)
28
+ * §3 mtime stale past `staleMs` — the staleness belt (always required, even for evicted)
29
+ * §3.2 not a current roster member — `droneState` !== 'present' (roster bonus; 'absent' by default)
30
+ */
31
+ export declare function selectOrphanInboxes(args: SelectOrphanInboxesArgs): OrphanInboxEntry[];
32
+ export interface InboxLivenessDeps {
33
+ /** raw `pgrep -f <inboxPath>` match — true if a tail process is following the file (heartbeat-independent). */
34
+ pgrepTailMatch: (inboxPath: string) => boolean;
35
+ /** mtime (ms) of the heartbeat sidecar, or null if absent/unreadable. */
36
+ readHeartbeatMtimeMs: (heartbeatPath: string) => number | null;
37
+ /** parsed PID from the pidfile, or null if absent/unparseable. */
38
+ readPidfilePid: (pidfilePath: string) => number | null;
39
+ /** kill(pid, 0) liveness: true if the process exists. */
40
+ isAlive: (pid: number) => boolean;
41
+ now: number;
42
+ heartbeatStaleMs?: number;
43
+ }
44
+ /**
45
+ * §2 live-safety check — the HARD gate. LIVE if ANY of three INDEPENDENT signals
46
+ * fire (so a single positive vetoes the delete):
47
+ * 1. a raw `tail` pgrep match (a wedged-but-present tail still holds the inode → KEEP)
48
+ * 2. a heartbeat sidecar present AND fresh (within the stale threshold)
49
+ * 3. a pidfile whose PID is alive (kill-0)
50
+ */
51
+ export declare function isInboxLive(inboxPath: string, deps: InboxLivenessDeps): boolean;
52
+ export interface OrphanGcDeps {
53
+ /** list the `<drone_id>.log` entries in the cube inbox dir (excludes sidecars). */
54
+ listInboxLogs: (cubeInboxDir: string) => OrphanInboxEntry[];
55
+ isLive: (inboxPath: string) => boolean;
56
+ droneState: (droneId: string) => DroneRosterState;
57
+ unlink: (path: string) => void;
58
+ now: number;
59
+ staleMs: number;
60
+ }
61
+ /**
62
+ * Wire the GC for one cube dir: select orphans (excluding the just-assimilated
63
+ * drone), then unlink each orphan's triplet (`.log` + `.monitor.pid` +
64
+ * `.monitor.heartbeat`). Best-effort — every unlink is swallowed per-file so a
65
+ * single failure never aborts the sweep or blocks assimilate. Returns the paths
66
+ * actually removed. Never rmdir's the cube dir (a live sibling may use it).
67
+ */
68
+ export declare function gcOrphanInboxesForCube(args: {
69
+ cubeInboxDir: string;
70
+ selfDroneId: string;
71
+ deps: OrphanGcDeps;
72
+ }): string[];
73
+ /** Real FS/process-backed deps, reusing the #795/#822 primitives. */
74
+ export declare function defaultInboxLivenessDeps(now?: number): InboxLivenessDeps;
75
+ /** Real directory lister: `<drone_id>.log` files only (skips `.monitor.*` sidecars). */
76
+ export declare function defaultListInboxLogs(cubeInboxDir: string): OrphanInboxEntry[];
77
+ //# sourceMappingURL=gc-orphan-inboxes.d.ts.map
@@ -0,0 +1 @@
1
+ import{spawnSync as d}from"node:child_process";import{readFileSync as m,readdirSync as p,statSync as c}from"node:fs";import{join as h}from"node:path";import{pidfilePathFor as l,heartbeatPathFor as f,HEARTBEAT_STALE_MS as M}from"./inbox-monitor.js";const y=720*60*60*1e3;function b(n){const{entries:t,isLive:e,droneState:r,now:o,staleMs:a}=n;return t.filter(i=>e(i.inboxPath)||o-i.mtimeMs<a?!1:r(i.droneId)!=="present")}function P(n,t){if(t.pgrepTailMatch(n))return!0;const e=t.readHeartbeatMtimeMs(f(n)),r=t.heartbeatStaleMs??M;if(e!==null&&t.now-e<r)return!0;const o=t.readPidfilePid(l(n));return!!(o!==null&&t.isAlive(o))}function L(n){const{cubeInboxDir:t,selfDroneId:e,deps:r}=n,o=r.listInboxLogs(t).filter(s=>s.droneId!==e),a=b({entries:o,isLive:r.isLive,droneState:r.droneState,now:r.now,staleMs:r.staleMs}),i=[];for(const s of a)for(const u of[s.inboxPath,l(s.inboxPath),f(s.inboxPath)])try{r.unlink(u),i.push(u)}catch{}return i}function v(n=Date.now()){return{pgrepTailMatch:t=>{try{const e=d("pgrep",["-f",t],{encoding:"utf-8",timeout:2e3});return e.error?!1:e.status===0&&e.stdout.trim().length>0}catch{return!1}},readHeartbeatMtimeMs:t=>{try{return c(t).mtimeMs}catch{return null}},readPidfilePid:t=>{try{const e=Number.parseInt(m(t,"utf8").trim(),10);return Number.isNaN(e)?null:e}catch{return null}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}},now:n}}function A(n){let t;try{t=p(n)}catch{return[]}const e=[];for(const r of t){if(!r.endsWith(".log"))continue;const o=h(n,r);try{e.push({droneId:r.slice(0,-4),inboxPath:o,mtimeMs:c(o).mtimeMs})}catch{}}return e}export{y as ORPHAN_INBOX_STALE_MS,v as defaultInboxLivenessDeps,A as defaultListInboxLogs,L as gcOrphanInboxesForCube,P as isInboxLive,b as selectOrphanInboxes};
@@ -102,7 +102,36 @@ export interface InboxLockDeps {
102
102
  removeIfContent(path: string, expected: string): void;
103
103
  /** kill(pid,0) liveness: true if the process exists (alive), false if gone (ESRCH). */
104
104
  isAlive(pid: number): boolean;
105
+ /**
106
+ * gh#840 (optional — enables the node-WEDGE reap): read the holder heartbeat
107
+ * sidecar for this pidfile's inbox → { mtimeMs, nonce } or null if absent /
108
+ * unreadable. Absent dep ⇒ no wedge reap (legacy behavior).
109
+ */
110
+ readHeartbeat?(pidfilePath: string): {
111
+ mtimeMs: number;
112
+ nonce: string;
113
+ } | null;
114
+ /** gh#840: clock for heartbeat staleness (injected for tests; defaults to Date.now). */
115
+ now?(): number;
116
+ /** gh#840: heartbeat staleness threshold; defaults to HEARTBEAT_STALE_MS. */
117
+ heartbeatStaleMs?: number;
105
118
  }
119
+ /** gh#840: pidfile content is `<pid>` (legacy) or `<pid>:<nonce>` (identity-tagged). */
120
+ export declare function parsePidfileContent(trimmed: string): {
121
+ pid: number;
122
+ nonce: string | null;
123
+ };
124
+ /**
125
+ * gh#840: is the LIVE pidfile holder node-WEDGED (reapable)? True ONLY when BOTH
126
+ * (a) the heartbeat sidecar mtime is stale past the threshold, AND (b) the
127
+ * heartbeat's nonce MATCHES the pidfile holder's nonce (same identity wrote
128
+ * both). A nonce MISMATCH ⇒ the stale heartbeat belongs to a DIFFERENT identity
129
+ * than the currently-alive pidfile holder (PID reuse, or a young reclaimer that
130
+ * hasn't written its first heartbeat yet) ⇒ NOT wedged ⇒ NEVER reap. Err toward
131
+ * NOT reaping: no readHeartbeat dep, no heartbeat file, or a legacy no-nonce
132
+ * pidfile all return false (a false-reap is the deafness we prevent).
133
+ */
134
+ export declare function isHolderWedged(pidfilePath: string, holderNonce: string | null, deps: InboxLockDeps): boolean;
106
135
  export declare function pidfilePathFor(inboxPath: string): string;
107
136
  /** gh#822: the holder-liveness heartbeat sidecar (mtime touched each tick). */
108
137
  export declare function heartbeatPathFor(inboxPath: string): string;
@@ -122,7 +151,23 @@ export declare function tailArgsFor(inboxPath: string, fromByteOffset: number |
122
151
  * delete) — only reaps a still-present provably-dead (ESRCH) / unparseable
123
152
  * pidfile, then re-claims.
124
153
  */
125
- export declare function acquireInboxLock(pidfilePath: string, ownPid: number, deps: InboxLockDeps, maxAttempts?: number): boolean;
154
+ export declare function acquireInboxLock(pidfilePath: string, ownPid: number, deps: InboxLockDeps, maxAttempts?: number, ownNonce?: string): boolean;
155
+ /**
156
+ * gh#840: read the holder heartbeat sidecar for a pidfile's inbox.
157
+ * Freshness = file mtime; identity = file content (the holder's nonce). Returns
158
+ * null if the sidecar is absent/unreadable.
159
+ */
160
+ export declare function readHeartbeatSidecar(pidfilePath: string): {
161
+ mtimeMs: number;
162
+ nonce: string;
163
+ } | null;
164
+ /**
165
+ * gh#840: write the holder heartbeat sidecar — the per-holder identity nonce as
166
+ * content; the FILE MTIME (touched on every write) is the freshness signal the
167
+ * SLI + the wedge reaper read. Replaces the old timestamp-as-content (nothing
168
+ * read that content; mtime was always the freshness source).
169
+ */
170
+ export declare function writeHeartbeat(heartbeatPath: string, nonce: string): void;
126
171
  /**
127
172
  * Is this module being invoked as the bin entry point?
128
173
  *
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as w}from"node:child_process";import{randomBytes as y}from"node:crypto";import{linkSync as T,readFileSync as p,realpathSync as v,statSync as k,unlinkSync as d,writeFileSync as I}from"node:fs";import{createInterface as O}from"node:readline";import{fileURLToPath as N}from"node:url";const L=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,_=1024;class R{cap;seen=new Set;order=[];constructor(e=_){if(this.cap=e,!Number.isInteger(e)||e<1)throw new Error("cap must be a positive integer")}remember(e){if(this.seen.has(e))return!1;for(this.seen.add(e),this.order.push(e);this.order.length>this.cap;){const r=this.order.shift();r&&this.seen.delete(r)}return!0}}function b(t){const e=L.exec(t);if(!e)return null;const[,,r,o,i]=e,n=i.trim();return`${r} (${o}): ${n}`}function $(t,e){const r=b(t);return r===null?null:e.remember(t)?r:null}function D(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let o;try{o=p(t,"utf-8")}catch(n){if(n?.code==="ENOENT")return;throw n}const i=o.split(/\r?\n/);i.at(-1)===""&&i.pop();for(const n of i.slice(-r))b(n)!==null&&e.remember(n)}function F(t,e,r,o){if(t<e.lastEmittedOffset)return{kind:"rotation",state:{lastEmittedOffset:t,grewSince:null}};if(t===e.lastEmittedOffset)return{kind:"ok",state:{lastEmittedOffset:e.lastEmittedOffset,grewSince:null}};const i=e.grewSince??r,n={lastEmittedOffset:e.lastEmittedOffset,grewSince:i};return r-i>=o?{kind:"respawn",state:n}:{kind:"ok",state:n}}function A(t){return`${t}.monitor.pid`}function C(t){return`${t}.monitor.heartbeat`}const h=3e4,M=5*h,J=5*h;function G(t,e){return e===null?["-F","-n","0",t]:["-F","-c",`+${e+1}`,t]}function E(t){try{return k(t).size}catch{return 0}}function P(t,e,r,o=3){const i=String(e);for(let n=0;n<o;n++){if(r.claim(t,i))return!0;const c=r.read(t);if(c===null)continue;const l=c.trim();if(l===""){r.removeIfContent(t,c);continue}const u=Number.parseInt(l,10);if(!Number.isNaN(u)&&r.isAlive(u))return!1;r.removeIfContent(t,c)}return!1}function H(){return{claim:(t,e)=>{const r=`${t}.tmp.${process.pid}.${y(6).toString("hex")}`;try{I(r,e,{mode:384});try{return T(r,t),!0}catch(o){if(o?.code==="EEXIST")return!1;throw o}}finally{try{d(r)}catch{}}},read:t=>{try{return p(t,"utf8")}catch{return null}},removeIfContent:(t,e)=>{try{p(t,"utf8")===e&&d(t)}catch{}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}}}}function K(){const t=process.argv[2];t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const e=A(t),r=H();P(e,process.pid,r)||process.exit(0);const o=()=>r.removeIfContent(e,String(process.pid)),i=new R;D(t,i);let n={lastEmittedOffset:E(t),grewSince:null},c=!1,l=null;const u=a=>{const s=w("tail",G(t,a),{stdio:["ignore","pipe","inherit"]});l=s,s.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),o(),process.exit(1)),O({input:s.stdout,crlfDelay:1/0}).on("line",f=>{const m=$(f,i);m!==null&&(console.log(m),n={lastEmittedOffset:E(t),grewSince:null})}),s.on("error",f=>{s===l&&(console.error(`borg-inbox-monitor: tail failed: ${f.message}`),o(),process.exit(1))}),s.on("exit",(f,m)=>{s===l&&(o(),c&&process.exit(0),m&&process.exit(0),process.exit(f??0))})};u(null);const S=C(t),x=setInterval(()=>{try{I(S,String(Date.now()),{mode:384})}catch{}const a=F(E(t),n,Date.now(),M);if(n=a.state,a.kind==="respawn"&&!c){const s=l;u(n.lastEmittedOffset);try{s?.kill("SIGKILL")}catch{}n={lastEmittedOffset:n.lastEmittedOffset,grewSince:null}}},h);x.unref();const g=a=>{if(c)return;c=!0,clearInterval(x);try{d(S)}catch{}o();const s=l;s&&!s.killed&&!s.kill(a)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref()};process.once("SIGTERM",()=>g("SIGTERM")),process.once("SIGINT",()=>g("SIGINT"))}function q(t,e){try{return v(t)===N(e)}catch{return!1}}q(process.argv[1],import.meta.url)&&K();export{J as HEARTBEAT_STALE_MS,_ as RECENT_EMITTED_LINE_CAP,R as RecentLineDeduper,P as acquireInboxLock,F as evaluateInboxTailStall,b as formatEventLine,$ as formatFreshEventLine,C as heartbeatPathFor,q as isEntryInvocation,A as pidfilePathFor,D as seedDeduperFromInboxTail,G as tailArgsFor};
2
+ import{spawn as O}from"node:child_process";import{randomBytes as I}from"node:crypto";import{linkSync as N,readFileSync as h,realpathSync as $,statSync as g,unlinkSync as x,writeFileSync as y}from"node:fs";import{createInterface as L}from"node:readline";import{fileURLToPath as M}from"node:url";const _=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,D=1024;class R{cap;seen=new Set;order=[];constructor(e=D){if(this.cap=e,!Number.isInteger(e)||e<1)throw new Error("cap must be a positive integer")}remember(e){if(this.seen.has(e))return!1;for(this.seen.add(e),this.order.push(e);this.order.length>this.cap;){const r=this.order.shift();r&&this.seen.delete(r)}return!0}}function T(t){const e=_.exec(t);if(!e)return null;const[,,r,n,o]=e,i=o.trim();return`${r} (${n}): ${i}`}function C(t,e){const r=T(t);return r===null?null:e.remember(t)?r:null}function F(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let n;try{n=h(t,"utf-8")}catch(i){if(i?.code==="ENOENT")return;throw i}const o=n.split(/\r?\n/);o.at(-1)===""&&o.pop();for(const i of o.slice(-r))T(i)!==null&&e.remember(i)}function H(t,e,r,n){if(t<e.lastEmittedOffset)return{kind:"rotation",state:{lastEmittedOffset:t,grewSince:null}};if(t===e.lastEmittedOffset)return{kind:"ok",state:{lastEmittedOffset:e.lastEmittedOffset,grewSince:null}};const o=e.grewSince??r,i={lastEmittedOffset:e.lastEmittedOffset,grewSince:o};return r-o>=n?{kind:"respawn",state:i}:{kind:"ok",state:i}}function A(t){const e=t.indexOf(":");return e===-1?{pid:Number.parseInt(t,10),nonce:null}:{pid:Number.parseInt(t.slice(0,e),10),nonce:t.slice(e+1)||null}}function G(t,e,r){if(!e||!r.readHeartbeat)return!1;const n=r.readHeartbeat(t);if(n===null)return!1;const o=r.now?r.now():Date.now(),i=r.heartbeatStaleMs??k;return o-n.mtimeMs>=i&&n.nonce===e}function P(t){return`${t}.monitor.pid`}function v(t){return`${t}.monitor.heartbeat`}const b=3e4,K=5*b,k=5*b;function q(t,e){return e===null?["-F","-n","0",t]:["-F","-c",`+${e+1}`,t]}function E(t){try{return g(t).size}catch{return 0}}function B(t,e,r,n=3,o){const i=o?`${e}:${o}`:String(e);for(let c=0;c<n;c++){if(r.claim(t,i))return!0;const l=r.read(t);if(l===null)continue;const a=l.trim();if(a===""){r.removeIfContent(t,l);continue}const{pid:f,nonce:p}=A(a);if(!Number.isNaN(f)&&r.isAlive(f)){if(G(t,p,r)){r.removeIfContent(t,l);continue}return!1}r.removeIfContent(t,l)}return!1}function U(){return{claim:(t,e)=>{const r=`${t}.tmp.${process.pid}.${I(6).toString("hex")}`;try{y(r,e,{mode:384});try{return N(r,t),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}}finally{try{x(r)}catch{}}},read:t=>{try{return h(t,"utf8")}catch{return null}},removeIfContent:(t,e)=>{try{h(t,"utf8")===e&&x(t)}catch{}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}},readHeartbeat:W,now:()=>Date.now(),heartbeatStaleMs:k}}function W(t){const e=t.replace(/\.monitor\.pid$/,""),r=v(e);try{return{mtimeMs:g(r).mtimeMs,nonce:h(r,"utf8").trim()}}catch{return null}}function X(t,e){y(t,e,{mode:384})}function Y(){const t=process.argv[2];t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const e=P(t),r=U(),n=I(16).toString("hex");B(e,process.pid,r,3,n)||process.exit(0);const o=()=>r.removeIfContent(e,`${process.pid}:${n}`),i=new R;F(t,i);let c={lastEmittedOffset:E(t),grewSince:null},l=!1,a=null;const f=u=>{const s=O("tail",q(t,u),{stdio:["ignore","pipe","inherit"]});a=s,s.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),o(),process.exit(1)),L({input:s.stdout,crlfDelay:1/0}).on("line",m=>{const d=C(m,i);d!==null&&(console.log(d),c={lastEmittedOffset:E(t),grewSince:null})}),s.on("error",m=>{s===a&&(console.error(`borg-inbox-monitor: tail failed: ${m.message}`),o(),process.exit(1))}),s.on("exit",(m,d)=>{s===a&&(o(),l&&process.exit(0),d&&process.exit(0),process.exit(m??0))})};f(null);const p=v(t),S=setInterval(()=>{try{X(p,n)}catch{}const u=H(E(t),c,Date.now(),K);if(c=u.state,u.kind==="respawn"&&!l){const s=a;f(c.lastEmittedOffset);try{s?.kill("SIGKILL")}catch{}c={lastEmittedOffset:c.lastEmittedOffset,grewSince:null}}},b);S.unref();const w=u=>{if(l)return;l=!0,clearInterval(S);try{x(p)}catch{}o();const s=a;s&&!s.killed&&!s.kill(u)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref()};process.once("SIGTERM",()=>w("SIGTERM")),process.once("SIGINT",()=>w("SIGINT"))}function j(t,e){try{return $(t)===M(e)}catch{return!1}}j(process.argv[1],import.meta.url)&&Y();export{k as HEARTBEAT_STALE_MS,D as RECENT_EMITTED_LINE_CAP,R as RecentLineDeduper,B as acquireInboxLock,H as evaluateInboxTailStall,T as formatEventLine,C as formatFreshEventLine,v as heartbeatPathFor,j as isEntryInvocation,G as isHolderWedged,A as parsePidfileContent,P as pidfilePathFor,W as readHeartbeatSidecar,F as seedDeduperFromInboxTail,q as tailArgsFor,X as writeHeartbeat};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",