borgmcp 1.1.6 → 1.1.8
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.
- package/README.md +6 -1
- package/dist/assimilate-cmd.js +23 -23
- package/dist/assimilate-deps.js +2 -2
- package/dist/claude.js +9 -9
- package/dist/codex-launch.d.ts +1 -1
- package/dist/codex-launch.js +1 -1
- package/dist/gc-orphan-inboxes.d.ts +8 -3
- package/dist/gc-orphan-inboxes.js +1 -1
- package/dist/inbox-monitor.d.ts +92 -21
- package/dist/inbox-monitor.js +3 -1
- package/dist/index.js +20 -20
- package/dist/readiness-probe.d.ts +5 -0
- package/dist/readiness-probe.js +1 -0
- package/dist/regen-format.d.ts +6 -3
- package/dist/regen-format.js +12 -12
- package/dist/regen.js +3 -3
- package/dist/startup-services.d.ts +17 -0
- package/dist/startup-services.js +1 -0
- package/dist/stream-owner.d.ts +7 -1
- package/dist/stream-owner.js +2 -2
- package/dist/stream-status.d.ts +9 -5
- package/dist/stream-status.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -173,7 +173,12 @@ If `borg_regen` or `borg_stream-status` reports a broken wake path, follow the
|
|
|
173
173
|
CLI-specific recovery it prints:
|
|
174
174
|
|
|
175
175
|
- Claude Code: arm the inbox monitor command. The monitor wakes the agent
|
|
176
|
-
session when another drone posts to the cube.
|
|
176
|
+
session when another drone posts to the cube. The printed command includes
|
|
177
|
+
its required worktree-local `--state-root`; keep that value intact so the
|
|
178
|
+
config inbox can remain read-only and monitor runtime files do not dirty Git.
|
|
179
|
+
If it reports a stale legacy `.monitor.pid` or `.monitor.heartbeat`, confirm
|
|
180
|
+
the old Monitor has stopped, remove those legacy files, and arm the printed
|
|
181
|
+
command again; the new Monitor deliberately never deletes them automatically.
|
|
177
182
|
- Codex: check the remote-control socket status, relaunch with `borg --cli codex`
|
|
178
183
|
or `borg assimilate --cli codex` if needed, and run `borg_regen` manually when
|
|
179
184
|
returning to the session if no wake arrived.
|
package/dist/assimilate-cmd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{dirname as
|
|
2
|
-
`),1}if(r.flags.worktree!==void 0){const t=
|
|
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=
|
|
1
|
+
import{dirname as J,basename as R}from"node:path";import{randomUUID as le}from"node:crypto";import{roleSlug as se,matchRoleByName as ce,pickDefaultRole as ue}from"./role-resolver.js";import{deriveCubeName as me,parseGitRemote as de,sanitizeRemoteUrl as fe}from"./cube-name.js";import{validateName as V}from"./name-validator.js";import{renderAssimilationWelcome as he}from"./assimilate-welcome.js";import{shellEscape as ge}from"./shell-escape.js";import{withCodexCwdArg as we}from"./codex-remote.js";import{buildAgentKickoffPrompt as be,buildKickoffWakePathClause as ke,recordCodexWakeTarget as ye,socketPathFromRemoteArgs as pe}from"./codex-launch.js";import{perWorktreeBranchName as F,adoptWorktree as ve,computeWorktreePath as X,localBranchExists as Q,isMerged as $e}from"./worktree-lifecycle.js";import{DroneEvictedError as Se}from"./drone-lifecycle.js";import{codexBorgSessionConfigArgs as xe}from"./launch-gate.js";import{inboxPathForDrone as Ce}from"./cubes.js";import{monitorStateRootForWorktree as Re}from"./inbox-monitor.js";import{resolveLaunchEnv as _e,resolveOllamaBaseUrl as Ee,parseModel as Ie}from"./model-presets.js";import{unlinkSync as Ne}from"node:fs";import{gcOrphanInboxesForCube as Ae,defaultListInboxLogs as Pe,defaultInboxLivenessDeps as Le,isInboxLive as Te,ORPHAN_INBOX_STALE_MS as De}from"./gc-orphan-inboxes.js";import{installBorgPlugin as Ue}from"./opencode-plugin.js";import{computeOpenCodePort as Oe,connectOpenCodeDrone as je,injectInitialKickoff as We}from"./opencode-drone.js";async function dt(r,e){if(r.role!==void 0){const t=V(r.role);if(!t.ok)return e.stderr(t.error+`
|
|
2
|
+
`),1}if(r.flags.worktree!==void 0){const t=V(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=me(o,n),n){const c=fe(n),f=c?de(c):null;c&&!f&&i&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${i}' as cube name
|
|
4
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 _=e.cwd();e.stderr(`Checking your cubes\u2026
|
|
5
5
|
`);let I;try{I=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
6
|
`),a=await e.runSetup(),I=await e.listCubes(a.apiUrl,a.token);else throw t}const N=I.find(t=>t.name===i);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.
|
|
@@ -11,38 +11,38 @@ import{dirname as z,basename as R}from"node:path";import{randomUUID as ae}from"n
|
|
|
11
11
|
Pass --template <name>, --no-template, or --yes (defaults to starter).
|
|
12
12
|
`),1;t="starter"}e.stderr(i?`Creating cube '${i}'\u2026
|
|
13
13
|
`:`Creating your cube\u2026
|
|
14
|
-
`),u=await e.createCube(a.apiUrl,a.token,t?{name:i??void 0,template:t}:{name:i??void 0}),A=!0}let g;if(r.role!==void 0){if(g=
|
|
14
|
+
`),u=await e.createCube(a.apiUrl,a.token,t?{name:i??void 0,template:t}:{name:i??void 0}),A=!0}let g;if(r.role!==void 0){if(g=ce(u.roles,r.role),!g){const t=u.roles.map(f=>f.name).join(", "),n=Fe(r.role,u.roles.map(f=>f.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{const t=new Set((u.drones??[]).map(n=>n.role_id));if(g=
|
|
17
|
-
`),1}const
|
|
18
|
-
`),1;const
|
|
16
|
+
`),1}}else{const t=new Set((u.drones??[]).map(n=>n.role_id));if(g=ue(u.roles,{isFirstDrone:A,occupiedRoleIds:t}),!g)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 S=await e.getActiveCube();let x;if(S&&r.flags.here)if(S.cubeId===u.id)x=S.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
|
|
18
|
+
`),1;const K=r.flags.worktree!==void 0||S!==null&&!r.flags.here,ee=x??S?.droneId??null,P=K?null:await e.getLaunchModel(u.id,o,ee),b=r.flags.model??P?.model??g.default_model??null,L=Ee(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
19
|
`),1}const m=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${u.name}' as ${g.name}\u2026
|
|
20
|
-
`);let s;try{s=await e.assimilate(a.apiUrl,a.token,{cube_id:u.id,role_id:g.id,hostname:e.getHostname(),agent_kind:m,model:b,...
|
|
20
|
+
`);let s;try{s=await e.assimilate(a.apiUrl,a.token,{cube_id:u.id,role_id:g.id,hostname:e.getHostname(),agent_kind:m,model:b,...x?{prior_drone_id:x}:{}})}catch(t){if(t instanceof Se&&x!=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
21
|
`),1;const n=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${n}
|
|
22
22
|
`),1}const k=u.roles.find(t=>t.id===s.role_id)??g;s.reattached?e.stderr(`re-attached to existing seat ${s.drone_label} (session token rotated, no new drone minted)
|
|
23
23
|
`):k.id!==g.id&&e.stderr(`Note: your invite didn't grant the "${g.name}" role \u2014 assimilated as "${k.name}" instead.
|
|
24
|
-
`);const
|
|
24
|
+
`);const te=K;let w=null;if(te){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
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 f=t.stdout.trim(),y=e.runSync("git",["rev-parse",n],o).stdout.trim();f!==y&&e.stderr(`note: local HEAD (${f.slice(0,7)}) differs from ${n} (${y.slice(0,7)}); new worktree will start on ${n}
|
|
28
|
-
`);const v=R(o),$=r.flags.worktree??
|
|
29
|
-
`),1;const E=e.homedir();let h=
|
|
28
|
+
`);const v=R(o),$=r.flags.worktree??se(k.name);if($.length===0)return e.stderr(`cannot derive a worktree name from role "${k.name}"; pass an explicit --worktree <name>
|
|
29
|
+
`),1;const E=e.homedir();let h=X(E,v,$),p=F(R(h),v),q=2;for(;e.pathExists(h)||He(e,o,h)||Q(e.runSync,o,p)&&!$e(e.runSync,o,p,n);)h=X(E,v,$,q),p=F(R(h),v),q++;e.mkdirp(J(h));const z=Q(e.runSync,o,p)?e.runSync("git",["worktree","add",h,p],o):e.runSync("git",["worktree","add","-b",p,h,n],o);if(z.status!==0)return e.stderr(`git worktree add failed: ${Z(z.stderr)}
|
|
30
30
|
`),1;e.stderr(`spawned sibling worktree at ${h} on branch ${p} (${n}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
|
|
31
|
-
`),e.chdir(h),e.stderr(
|
|
31
|
+
`),e.chdir(h),e.stderr(Me(h,p,o)),w=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
32
|
`),w){const c=e.runSync("git",["worktree","remove","--force",w],o);c.status===0?e.stderr(`rolled back spawned worktree at ${w}
|
|
33
|
-
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${w}\` (rollback attempt failed: ${
|
|
34
|
-
`)}return 1}try{const t=
|
|
35
|
-
`)}if(!w){e.runSync("git",["fetch","origin","--prune"],d);const t=
|
|
36
|
-
`),e.stderr(
|
|
33
|
+
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${w}\` (rollback attempt failed: ${Z(c.stderr).trim()||"unknown"})
|
|
34
|
+
`)}return 1}const d=e.cwd(),T=e.findProjectRoot(d),D=Re(T);try{const t=Le(),n=J(Ce(s.cube_id,s.drone_id));Ae({cubeInboxDir:n,selfDroneId:s.drone_id,deps:{listInboxLogs:Pe,isLive:c=>Te(c,t,D),droneState:()=>"absent",unlink:c=>Ne(c),now:t.now,staleMs:De},monitorStateRoot:D})}catch{}e.setTerminalTitle(s.drone_label,u.name);const re=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(he(k.name,u.name,re));try{e.installProjectSessionHook(d)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${d}; it will be re-attempted on the next borg launch
|
|
35
|
+
`)}if(!w){e.runSync("git",["fetch","origin","--prune"],d);const t=F(R(d),R(o)),n=ve(e.runSync,d,t,"origin/main");n.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
|
|
36
|
+
`),e.stderr(Be(d,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 ${m} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
|
|
38
|
-
`);const
|
|
39
|
-
`),
|
|
38
|
+
`);const ne=e.getInboxPath(s.cube_id,s.drone_id),U=m==="codex"?`borg-wake-${le()}`:null,oe=ke(m,m==="claude"?ne:null,m==="claude"?D:null);let O,Y=[],C,j=null,W=null;b?await e.setLaunchModel(s.cube_id,T,{model:b,ollamaBaseUrl:Ie(b).kind==="ollama"?L:null}):await e.clearLaunchModel(s.cube_id,T);const G=_e(b,L),M={...process.env,...G.set,BORG_SESSION:"1"};for(const t of G.unset)delete M[t];if(m==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
|
|
39
|
+
`),O="\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."):O="Codex wake-path capability check passed: remote-control socket established for this session.",Y=t.args,Object.keys(t.env).length>0&&Object.assign(M,t.env),j=pe(t.args),W=t.server?.cleanup??null}const B=be({cli:m,codexWakeNonce:U,monitorClause:oe,codexWakePathClause:O});let H;C=[B],m==="codex"?C=[...xe(),...Y,...we(C,d)]:m==="opencode"&&(H=Oe(s.drone_id),Ue(),C=[d,"--port",String(H),"--auto","--prompt",B]);const ie=e.exec(m,C,d,M);if(m==="codex"&&j&&U&&ye({deps:e,cubeId:s.cube_id,droneId:s.drone_id,socketPath:j,cwd:d,previewNeedle:U,launchedAtSeconds:Math.floor(Date.now()/1e3)}),m==="opencode"){const t=`http://127.0.0.1:${H}`;je({serverUrl:t,directory:d,droneLabel:s.drone_label,cubeName:i??"borg"}).then(()=>We(B)).catch(()=>{})}const ae=await ie;if(W)try{W()}catch{}return w&&_!==w&&e.stderr(`
|
|
40
40
|
Agent exited. You were working in ${w}; your shell is back in ${_}.
|
|
41
41
|
To return:
|
|
42
|
-
cd ${
|
|
43
|
-
`),
|
|
42
|
+
cd ${ge(w)}
|
|
43
|
+
`),ae}function Me(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
|
|
45
|
+
`}function Be(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
|
|
48
|
-
`).some(i=>i===`worktree ${a}`)}function
|
|
47
|
+
`}function Z(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function He(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 Fe(r,e){if(e.length===0)return null;const a=r.toLowerCase();let o=null;for(const i of e){const l=Ke(a,i.toLowerCase());l<=2&&(o===null||l<o.distance)&&(o={name:i,distance:l})}return o?o.name:null}function Ke(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[i-1]===e[l-1]?0:1;o[l]=Math.min(o[l-1]+1,a[l]+1,a[l-1]+_)}for(let l=0;l<=e.length;l++)a[l]=o[l]}return a[e.length]}export{dt as runAssimilate,Z as safeStderr,Fe as suggestRoleName};
|
package/dist/assimilate-deps.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{spawnSync as p,spawn as l}from"node:child_process";import{existsSync as h,mkdirSync as f}from"node:fs";import{hostname as b,homedir as _}from"node:os";import{createInterface as d}from"node:readline/promises";import{API_URL as u,getValidToken as m,listCubes as
|
|
1
|
+
import{spawnSync as p,spawn as l}from"node:child_process";import{existsSync as h,mkdirSync as f}from"node:fs";import{hostname as b,homedir as _}from"node:os";import{createInterface as d}from"node:readline/promises";import{readinessProbeEnv as C}from"./readiness-probe.js";import{API_URL as u,getValidToken as m,listCubes as T,getCube as y,createCube as k,assimilate as w,listTemplates as g}from"./remote-client.js";import{findProjectRoot as L,getActiveCube as v,setActiveCube as x,inboxPathForDrone as M,setCodexWakeTarget as S,getLaunchModel as R,setLaunchModel as P,clearLaunchModel as A}from"./cubes.js";import{authenticateWithGoogle as U}from"./auth.js";import{addProjectSessionStartHook as j}from"./config-utils.js";import{setTerminalTitle as D}from"./terminal-title.js";import{defaultCliChoiceDeps as G,resolveCliChoice as H}from"./cli-platform.js";import{prepareCodexRemoteLaunch as I,defaultCodexRemoteDeps as q}from"./codex-remote.js";import{findLoadedCodexThread as E}from"./codex-app-server.js";import{checkModelReachable as V}from"./model-presets.js";function te(){return{runSync:(e,r,t)=>{const o=p(e,r,{cwd:t,encoding:"utf-8"});return{status:o.status,stdout:o.stdout??"",stderr:o.stderr??""}},pathExists:e=>h(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),homedir:()=>_(),mkdirp:e=>f(e,{recursive:!0}),exec:(e,r,t,o)=>new Promise((s,i)=>{const a=l(e,r,{cwd:t,stdio:"inherit",shell:!1,env:o??process.env});a.on("error",i),a.on("exit",n=>s(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=d({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>b(),setTerminalTitle:(e,r)=>{D({label:e,cubeName:r},r)},getActiveCube:()=>v(),setActiveCube:e=>x(e),findProjectRoot:e=>L(e),installProjectSessionHook:e=>{j(e)},getCachedAuth:async()=>{try{return{token:await m(),apiUrl:u}}catch{return null}},runSetup:async()=>(await U(),{token:await m(),apiUrl:u}),listCubes:async(e,r)=>{const{cubes:t}=await T();return t.map(o=>({id:o.id,name:o.name}))},getCube:async(e,r,t)=>{const o=await y(t);return{id:o.id,name:o.name,roles:o.roles,drones:(o.drones??[]).map(s=>({role_id:s.role_id}))}},createCube:async(e,r,t)=>{const o=await k(t.name,"",t.template?{template:t.template}:void 0);return{id:o.id,name:o.name,roles:o.roles}},assimilate:async(e,r,t)=>{const o=await w({cube_id:t.cube_id,role_id:t.role_id,...t.prior_drone_id?{prior_drone_id:t.prior_drone_id}:{},...t.model!=null?{model:t.model}:{}},void 0,t.hostname??null,t.agent_kind??null);return{cube_id:o.cube.id,drone_id:o.drone.id,drone_label:o.drone.label,session_token:o.sessionToken,role_id:o.role.id,reattached:o.reattached===!0}},listTemplates:async(e,r)=>{const{templates:t}=await g();return t.map(o=>({name:o.name,description:o.description}))},getInboxPath:(e,r)=>M(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1,env:C()});let t="",o=!1;const s=n=>{if(!o){o=!0;try{r.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>s(!1),2e3);r.on("error",()=>{clearTimeout(i),s(!1)}),r.on("exit",()=>{clearTimeout(i),s(o)}),r.stdout?.on("data",n=>{t+=n.toString("utf-8");for(const c of t.split(`
|
|
2
2
|
`))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(i),s(!0);return}});const a=JSON.stringify({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"borg-assimilate-probe",version:"0.9.3"}}});try{r.stdin?.write(a+`
|
|
3
|
-
`)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>
|
|
3
|
+
`)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>H(e,G(async r=>{const t=d({input:process.stdin,output:process.stdout});try{return await t.question(r)}finally{t.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>I(q()),setCodexWakeTarget:S,findLoadedCodexThread:E,fetch,checkModelReachable:V,getLaunchModel:(e,r,t)=>R(e,r,t),setLaunchModel:(e,r,t)=>P(e,r,t),clearLaunchModel:(e,r)=>A(e,r)}}export{te as buildDefaultAssimilateDeps};
|
package/dist/claude.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as
|
|
3
|
-
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(
|
|
4
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
5
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await
|
|
6
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await
|
|
7
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
8
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1)),
|
|
9
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
2
|
+
import{spawn as B}from"child_process";import{randomUUID as W}from"node:crypto";import{basename as _}from"node:path";import{createInterface as G}from"node:readline/promises";import i from"chalk";import{findProjectRoot as b,getActiveCube as U,getLaunchModel as Y,inboxPathForDrone as K,setCodexWakeTarget as V,pruneDeadCodexWakeTargets as j}from"./cubes.js";import{monitorStateRootForWorktree as q}from"./inbox-monitor.js";import{applyOllamaLaunchEnv as X,checkModelReachable as z}from"./model-presets.js";import{handleVersionFlag as J,getPackageVersion as w}from"./version.js";import{isHelpFlag as L,setupHelpText as Q,topLevelHelpText as Z,assimilateHelpText as ee}from"./cli-help.js";import{runSpawn as re}from"./spawn.js";import{buildClaudeLaunchArgs as oe}from"./claude-launch-args.js";import{parseSyncArgs as se,runSync as te}from"./sync.js";import{parseCleanupArgs as ie,runCleanup as ae}from"./cleanup-cmd.js";import{parseAssimilateArgs as ne}from"./parse-assimilate-args.js";import{runAssimilate as ce}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as le}from"./assimilate-deps.js";import{parseLaunchAllArgs as R}from"./parse-launch-all-args.js";import{unknownSubcommand as de}from"./unknown-subcommand.js";import{runLaunchAll as O}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as k}from"./launch-all-deps.js";import{discoverDroneCandidates as pe}from"./launch-all-discovery.js";import{explicitCliLaunchHint as ue,runBareLaunchMenu as me,shouldResolveExplicitCliLaunchHintTargets as fe,shouldShowLaunchMenu as ge}from"./bare-launch-menu.js";import{setTerminalTitle as he}from"./terminal-title.js";import{initConsolePrefix as we,consolePrefix as t}from"./console-prefix.js";import{initDebugFromArgv as Ce}from"./debug.js";import{fetchLatestBorgmcpVersion as xe,compareVersionsForStaleness as ve}from"./stale-version-check.js";import{defaultCliChoiceDeps as be,detectCliAvailability as $,installedCliNames as D,parseCliFlag as ke,resolveCliChoice as $e}from"./cli-platform.js";import{getRefreshToken as Se,getIdToken as ye}from"./config.js";import{composeGetStarted as Ae,shouldShowGetStarted as Te}from"./get-started.js";import{prepareCodexRemoteLaunch as Pe,withCodexCwdArg as Ie,defaultCodexRemoteDeps as Le,checkCodexBridgeHealthy as Re}from"./codex-remote.js";import{findLoadedCodexThread as Oe}from"./codex-app-server.js";import{buildAgentKickoffPrompt as De,buildKickoffWakePathClause as Me,recordCodexWakeTarget as Ee,socketPathFromRemoteArgs as M}from"./codex-launch.js";import{codexBorgSessionConfigArgs as He}from"./launch-gate.js";import{addCodexMcpServer as Ne,addCodexSessionStartHook as Fe,addCodexUserPromptSubmitHook as Be,addMcpServer as We,addOpenCodeMcpServer as _e,addProjectSessionStartHook as Ge,addUserPromptSubmitHook as Ue,isCodexMcpServerConfigured as Ye,isMcpServerConfigured as Ke,isOpenCodeMcpServerConfigured as Ve,removeSessionStartHook as je}from"./config-utils.js";import{installBorgPlugin as qe}from"./opencode-plugin.js";import{connectOpenCodeDrone as Xe,computeOpenCodePort as E,injectInitialKickoff as ze}from"./opencode-drone.js";async function Je(){Ce(process.argv),J(),await we();const c=(async()=>{if(!process.stderr.isTTY)return;const e=w(),r=await xe();if(!r)return;const a=ve(e,r);a.stale&&a.message&&process.stderr.write(`${t()}${a.message}
|
|
3
|
+
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(Z(w())),process.exit(0)),process.argv[2]==="setup"){L(process.argv[3])&&(process.stdout.write(Q(w())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){process.argv.slice(3).some(L)&&(process.stdout.write(ee(w())),process.exit(0));const e=ne(process.argv.slice(3));e.ok||(process.stderr.write(i.red(`${t()}\u25FC borg assimilate: ${e.error}
|
|
4
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=le(),a=await ce({role:e.role,flags:e.flags},r);process.exit(a)}if(process.argv[2]==="spawn"){const e=await re();process.exit(e)}if(process.argv[2]==="sync"){const e=se(process.argv.slice(3));e.ok||(process.stderr.write(i.red(`${t()}\u25FC borg sync: ${e.error}
|
|
5
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await te({},e.options);process.exit(r)}if(process.argv[2]==="cleanup"){const e=ie(process.argv.slice(3));e.ok||(process.stderr.write(i.red(`${t()}\u25FC borg cleanup: ${e.error}
|
|
6
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await ae({},e.options);process.exit(r)}if(process.argv[2]==="launch-all"){const e=R(process.argv.slice(3));e.ok||(process.stderr.write(i.red(`${t()}\u25FC borg launch-all: ${e.error}
|
|
7
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=k(),a=await O(e.args,r);process.exit(a)}const n=de(process.argv[2]);if(n!==null&&(process.stderr.write(i.red(`${t()}\u25FC unknown command: ${n}
|
|
8
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1)),Te(await Se()!==null,await ye()!==null)){const e=D($()).length>0;process.stdout.write(Ae(e)),process.exit(0)}const d=ke(process.argv.slice(2));d.error&&(process.stderr.write(i.red(`${t()}\u25FC ${d.error}
|
|
9
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const S=async e=>{const r=G({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}};let s=await $e(d.cli,be(S,()=>process.stdin.isTTY===!0));Qe();const o=await U();let C;const y=async()=>o?(C===void 0&&(C=(await pe({targetCubeId:o.cubeId},k())).length>0),C):!1,x=process.stdin.isTTY===!0,v=process.stdout.isTTY===!0,A=ue({explicitCli:d.cli,stdinIsTTY:x,stdoutIsTTY:v,hasActiveCube:o!==null,hasLaunchAllTargets:fe({explicitCli:d.cli,stdinIsTTY:x,stdoutIsTTY:v,hasActiveCube:o!==null})?await y():!1});if(A&&process.stderr.write(A),ge({extraArgs:process.argv.slice(2),stdinIsTTY:x,stdoutIsTTY:v})){const e=D($()).filter(a=>a!==s),r=await me({defaultCli:s,otherInstalledClis:e,hasLaunchAllTargets:await y()},S);if(r.kind==="launch-all"){const a=R([]),F=a.ok?await O(a.args,k()):1;process.exit(F)}s=r.cli}const l=d.rest;he(o?{label:o.droneLabel,cubeName:o.name}:null,_(process.cwd()));const H=Me(s,o&&s==="claude"?K(o.cubeId,o.droneId):null,o&&s==="claude"?q(b(process.cwd())):null);await Promise.race([c,new Promise(e=>setTimeout(e,2e3))]);const T=s==="codex"?`borg-wake-${W()}`:null;let g,P=[],p={...process.env,BORG_SESSION:"1"},u=null,m=null;if(s==="codex"&&!l.includes("--remote")){console.error(`${t()}${i.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await Pe(Le());e.warning?(console.error(`${t()}${i.yellow(`warning: ${e.warning}`)}`),g="\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."):g="Codex wake-path capability check passed: remote-control socket established for this session.",P=e.args,p={...process.env,...e.env,BORG_SESSION:"1"},u=M(e.args),m=e.server?.cleanup??null}else s==="codex"&&l.includes("--remote")&&(g="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg_regen manually when returning to the session.",u=M(l),u&&(p={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));if(o){const e=await Y(o.cubeId,b(),o.droneId),r=X(p,e,process.env);if(p=r.env,r.probe){const a=await z(r.probe.descriptor,fetch,r.probe.baseUrl);a.ok||console.error(`${t()}${i.yellow(`warning: ${a.message}`)}`)}}const f=De({cli:s,codexWakeNonce:T,monitorClause:H,codexWakePathClause:g});let h;if(s==="codex")h=[...He(),...P,...Ie([...l,f],process.cwd())];else if(s==="opencode"){const e=o?E(o.droneId):14096;qe(),h=[process.cwd(),"--port",String(e),"--auto","--prompt",f,...l]}else h=oe(l,f);const N=s==="claude"?"Claude Code":s==="codex"?"Codex":"OpenCode";console.error(`${t()}${i.blue(`\u25FC Launching ${N}\u2026`)}`);const I=B(s,h,{stdio:"inherit",shell:!1,env:p});if(s==="opencode"){const r=`http://127.0.0.1:${o?E(o.droneId):14096}`;Xe({serverUrl:r,directory:process.cwd(),droneLabel:o?.droneLabel??"opencode",cubeName:o?.name??"borg"}).then(()=>ze(f)).catch(()=>{})}s==="codex"&&o&&u&&(Ee({deps:{setCodexWakeTarget:V,findLoadedCodexThread:Oe},cubeId:o.cubeId,droneId:o.droneId,socketPath:u,passthroughArgs:l,previewNeedle:T??f.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),j(e=>Re(e))),I.on("error",e=>{if(m)try{m()}catch{}if(e.code==="ENOENT"){console.error(`${t()}${i.red(`
|
|
10
10
|
\u25FC Failed to launch ${s}`)}`);const r=s==="opencode"?"opencode":s;console.error(`${t()}${i.gray(`Make sure ${r} is installed.
|
|
11
11
|
`)}`)}else console.error(`${t()}${i.red(`
|
|
12
12
|
\u25FC Failed to launch ${s}: ${e.message}
|
|
13
|
-
`)}`);process.exit(1)}),
|
|
13
|
+
`)}`);process.exit(1)}),I.on("exit",e=>{if(m)try{m()}catch{}process.exit(e??0)})}function Qe(){const c=$();if(c.claude)try{Ke()||We(),Ge(b(process.cwd())),je(),Ue()}catch(n){console.error(`${t()}${i.yellow(`warning: Claude Code integration check failed: ${n?.message??n}`)}`)}if(c.codex)try{Ye()||Ne(),Fe(),Be()}catch(n){console.error(`${t()}${i.yellow(`warning: Codex integration check failed: ${n?.message??n}`)}`)}if(c.opencode)try{Ve()||_e()}catch(n){console.error(`${t()}${i.yellow(`warning: OpenCode integration check failed: ${n?.message??n}`)}`)}}Je().catch(c=>{console.error(`${t()}${i.red(`
|
|
14
14
|
\u25FC Error: ${c.message}
|
|
15
15
|
`)}`),process.exit(1)});
|
package/dist/codex-launch.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type AgentKind } from './regen-format.js';
|
|
|
9
9
|
* `monitorClause`. Codex wakes via the app-server (no tail-Monitor / `/loop`
|
|
10
10
|
* to arm) → empty; no active cube (no inboxPath) → empty.
|
|
11
11
|
*/
|
|
12
|
-
export declare function buildKickoffWakePathClause(agentKind: AgentKind, inboxPath: string | null): string;
|
|
12
|
+
export declare function buildKickoffWakePathClause(agentKind: AgentKind, inboxPath: string | null, monitorStateRoot?: string | null): string;
|
|
13
13
|
export interface CodexWakeTargetDeps {
|
|
14
14
|
setCodexWakeTarget: (cubeId: string, droneId: string, target: {
|
|
15
15
|
threadId: string;
|
package/dist/codex-launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{wakePathArming as c}from"./regen-format.js";function i(e,t){return e==="claude"&&t?c("claude",t)+`
|
|
1
|
+
import{wakePathArming as c}from"./regen-format.js";function i(e,t,r){return e==="claude"&&t?c("claude",t,r)+`
|
|
2
2
|
Keep this Monitor armed for the whole session \u2014 NEVER TaskStop it (the generic /loop "TaskStop any Monitor you armed" step does NOT apply to the cube inbox Monitor; it targets throwaway loop-scratch watches only). The sole exception is a confirmed terminal eviction (410 DRONE_EVICTED). `:""}function d(e){const t=e.codexWakeNonce?`Wake target nonce: ${e.codexWakeNonce}. `:"",r=e.codexWakePathClause??"Codex Borg wakeups use remote-control when available; if no wake arrives, run borg_regen manually when returning to the session.",a="OpenCode wakes: the inbox Monitor is not available yet for OpenCode; check activity by calling borg_read-log periodically.",o=e.cli==="claude"?"/loop ":"",n=e.cli==="claude"?e.monitorClause:e.cli==="codex"?r:a;return`${o}Call borg_regen and follow the playbook in its response. `+t+'Note: at session start the borg MCP server is still spinning up in parallel \u2014 if a system reminder claims "MCP server disconnected" or the borg tools are not yet registered, do NOT bail. Recover via `ToolSearch({query: "select:mcp__borg__borg_regen,mcp__borg__borg_log,Monitor", max_results: 3})` to load the bootstrap tools in one call, then call borg_regen. The server typically becomes available within a few seconds. '+n}function u(e){const t=e.indexOf("--remote");if(t<0)return null;const r=e[t+1];return r?.startsWith("unix://")?r.slice(7):null}function l(e){if(e[0]==="resume"&&e[1]&&!e[1].startsWith("-"))return e[1];const t=e.indexOf("--resume");return t>=0&&e[t+1]?e[t+1]:null}async function h(e){try{const t=e.passthroughArgs?l(e.passthroughArgs):null;if(t){await e.deps.setCodexWakeTarget(e.cubeId,e.droneId,{threadId:t,socketPath:e.socketPath});return}const r=Date.now()+15e3;for(;Date.now()<r;){const a=await e.deps.findLoadedCodexThread({socketPath:e.socketPath,cwd:e.cwd,previewIncludes:e.previewNeedle,updatedAfter:e.launchedAtSeconds-5});if(a){await e.deps.setCodexWakeTarget(e.cubeId,e.droneId,{threadId:a,socketPath:e.socketPath});return}await new Promise(o=>setTimeout(o,500))}}catch{}}export{d as buildAgentKickoffPrompt,i as buildKickoffWakePathClause,h as recordCodexWakeTarget,u as socketPathFromRemoteArgs,l as threadIdFromPassthroughArgs};
|
|
@@ -48,7 +48,7 @@ export interface InboxLivenessDeps {
|
|
|
48
48
|
* 2. a heartbeat sidecar present AND fresh (within the stale threshold)
|
|
49
49
|
* 3. a pidfile whose PID is alive (kill-0)
|
|
50
50
|
*/
|
|
51
|
-
export declare function isInboxLive(inboxPath: string, deps: InboxLivenessDeps): boolean;
|
|
51
|
+
export declare function isInboxLive(inboxPath: string, deps: InboxLivenessDeps, monitorStateRoot?: string | null): boolean;
|
|
52
52
|
export interface OrphanGcDeps {
|
|
53
53
|
/** list the `<drone_id>.log` entries in the cube inbox dir (excludes sidecars). */
|
|
54
54
|
listInboxLogs: (cubeInboxDir: string) => OrphanInboxEntry[];
|
|
@@ -60,14 +60,19 @@ export interface OrphanGcDeps {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Wire the GC for one cube dir: select orphans (excluding the just-assimilated
|
|
63
|
-
* drone), then unlink each orphan's
|
|
64
|
-
*
|
|
63
|
+
* drone), then unlink each orphan's inbox plus its derived worktree-runtime
|
|
64
|
+
* PID/heartbeat state. Legacy inbox-adjacent artifacts are intentionally left
|
|
65
|
+
* for explicit operator cleanup: GC must never race an old binary that does
|
|
66
|
+
* not participate in modern state serialization. Best-effort — every unlink is
|
|
67
|
+
* swallowed per-file so a
|
|
65
68
|
* single failure never aborts the sweep or blocks assimilate. Returns the paths
|
|
66
69
|
* actually removed. Never rmdir's the cube dir (a live sibling may use it).
|
|
67
70
|
*/
|
|
68
71
|
export declare function gcOrphanInboxesForCube(args: {
|
|
69
72
|
cubeInboxDir: string;
|
|
70
73
|
selfDroneId: string;
|
|
74
|
+
/** Explicit current-worktree root; legacy sidecars remain untouched. */
|
|
75
|
+
monitorStateRoot?: string | null;
|
|
71
76
|
deps: OrphanGcDeps;
|
|
72
77
|
}): string[];
|
|
73
78
|
/** Real FS/process-backed deps, reusing the #795/#822 primitives. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawnSync as
|
|
1
|
+
import{spawnSync as p}from"node:child_process";import{readFileSync as m,readdirSync as M,statSync as l}from"node:fs";import{join as b}from"node:path";import{pidfilePathFor as f,heartbeatPathFor as h,legacyPidfilePathFor as P,legacyHeartbeatPathFor as S,HEARTBEAT_STALE_MS as g}from"./inbox-monitor.js";const v=720*60*60*1e3;function x(r){const{entries:t,isLive:e,droneState:o,now:n,staleMs:a}=r;return t.filter(s=>e(s.inboxPath)||n-s.mtimeMs<a?!1:o(s.droneId)!=="present")}function A(r,t,e){if(t.pgrepTailMatch(r))return!0;const o=t.heartbeatStaleMs??g,n=[h(r,e)],a=[f(r,e)];e&&(n.push(S(r)),a.push(P(r)));for(const s of new Set(n)){const i=t.readHeartbeatMtimeMs(s);if(i!==null&&t.now-i<o)return!0}for(const s of new Set(a)){const i=t.readPidfilePid(s);if(i!==null&&t.isAlive(i))return!0}return!1}function E(r){const{cubeInboxDir:t,selfDroneId:e,monitorStateRoot:o,deps:n}=r,a=n.listInboxLogs(t).filter(c=>c.droneId!==e),s=x({entries:a,isLive:n.isLive,droneState:n.droneState,now:n.now,staleMs:n.staleMs}),i=[];for(const c of s){const d=[f(c.inboxPath,o),h(c.inboxPath,o)];for(const u of new Set([c.inboxPath,...d]))try{n.unlink(u),i.push(u)}catch{}}return i}function F(r=Date.now()){return{pgrepTailMatch:t=>{try{const e=p("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 l(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:r}}function N(r){let t;try{t=M(r)}catch{return[]}const e=[];for(const o of t){if(!o.endsWith(".log"))continue;const n=b(r,o);try{e.push({droneId:o.slice(0,-4),inboxPath:n,mtimeMs:l(n).mtimeMs})}catch{}}return e}export{v as ORPHAN_INBOX_STALE_MS,F as defaultInboxLivenessDeps,N as defaultListInboxLogs,E as gcOrphanInboxesForCube,A as isInboxLive,x as selectOrphanInboxes};
|
package/dist/inbox-monitor.d.ts
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
* entry surfaces, which is the part that summarizes the entry.
|
|
24
24
|
*
|
|
25
25
|
* Usage:
|
|
26
|
-
* borg-inbox-monitor <inbox-file-path>
|
|
26
|
+
* borg-inbox-monitor --state-root <worktree-runtime-root> <inbox-file-path>
|
|
27
|
+
*
|
|
28
|
+
* The state-root form is the supported launch path. The legacy positional-only
|
|
29
|
+
* form remains accepted for old hand-authored Monitor commands, and keeps its
|
|
30
|
+
* inbox-adjacent sidecars for compatibility while fleets transition.
|
|
27
31
|
*/
|
|
28
32
|
export declare const RECENT_EMITTED_LINE_CAP = 1024;
|
|
29
33
|
export declare class RecentLineDeduper {
|
|
@@ -94,10 +98,11 @@ export interface InboxLockDeps {
|
|
|
94
98
|
/** File contents, or null if absent/unreadable. */
|
|
95
99
|
read(path: string): string | null;
|
|
96
100
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
+
* Verify-then-unlink the file when its content equals `expected`.
|
|
102
|
+
*
|
|
103
|
+
* This primitive is NOT an atomic filesystem compare-and-swap. Callers must
|
|
104
|
+
* hold the per-inbox modern mutation lock around any destructive use; it is
|
|
105
|
+
* never used to mutate legacy inbox-adjacent artifacts.
|
|
101
106
|
*/
|
|
102
107
|
removeIfContent(path: string, expected: string): void;
|
|
103
108
|
/** kill(pid,0) liveness: true if the process exists (alive), false if gone (ESRCH). */
|
|
@@ -132,9 +137,35 @@ export declare function parsePidfileContent(trimmed: string): {
|
|
|
132
137
|
* pidfile all return false (a false-reap is the deafness we prevent).
|
|
133
138
|
*/
|
|
134
139
|
export declare function isHolderWedged(pidfilePath: string, holderNonce: string | null, deps: InboxLockDeps): boolean;
|
|
135
|
-
|
|
140
|
+
/**
|
|
141
|
+
* gh#979: a worktree is the durable local identity for a seat. Drone UUIDs
|
|
142
|
+
* change on re-mint, but a reused worktree must retain the same monitor
|
|
143
|
+
* runtime home. Keep it inside the worktree (where workspace-only sandboxes
|
|
144
|
+
* can write), never under TMPDIR/XDG, and make its contents self-ignored so
|
|
145
|
+
* runtime lock churn never dirties the repository.
|
|
146
|
+
*/
|
|
147
|
+
export declare function monitorStateRootForWorktree(worktreePath: string): string;
|
|
148
|
+
/** Legacy sidecars written by pre-gh#979 monitors beside the config inbox. */
|
|
149
|
+
export declare function legacyPidfilePathFor(inboxPath: string): string;
|
|
150
|
+
export declare function legacyHeartbeatPathFor(inboxPath: string): string;
|
|
151
|
+
/**
|
|
152
|
+
* State paths are keyed by the absolute inbox path within the explicitly
|
|
153
|
+
* supplied worktree runtime root. Omitting the root intentionally preserves
|
|
154
|
+
* the legacy inbox-adjacent layout for old manual commands; supported launch
|
|
155
|
+
* and orientation paths always pass the root explicitly.
|
|
156
|
+
*/
|
|
157
|
+
export declare function pidfilePathFor(inboxPath: string, stateRoot?: string | null): string;
|
|
136
158
|
/** gh#822: the holder-liveness heartbeat sidecar (mtime touched each tick). */
|
|
137
|
-
export declare function heartbeatPathFor(inboxPath: string): string;
|
|
159
|
+
export declare function heartbeatPathFor(inboxPath: string, stateRoot?: string | null): string;
|
|
160
|
+
/**
|
|
161
|
+
* Prepare a private, worktree-local monitor runtime root. The supplied root
|
|
162
|
+
* must have the exact `<worktree>/.borgmcp/inbox-monitor` shape generated by
|
|
163
|
+
* `monitorStateRootForWorktree()`. Before any write, resolve the saved
|
|
164
|
+
* worktree canonically and reject a symlinked `.borgmcp` or `inbox-monitor`
|
|
165
|
+
* ancestor. Its local `.gitignore` ignores itself and all descendants, so
|
|
166
|
+
* runtime state produces no repository dirt without mutating tracked ignores.
|
|
167
|
+
*/
|
|
168
|
+
export declare function ensureMonitorStateDir(stateRoot: string): string;
|
|
138
169
|
export declare const HEARTBEAT_STALE_MS: number;
|
|
139
170
|
/**
|
|
140
171
|
* gh#822: `tail` args — ARM (`-n 0`, skip history, matches the prior shape) vs
|
|
@@ -146,12 +177,46 @@ export declare function tailArgsFor(inboxPath: string, fromByteOffset: number |
|
|
|
146
177
|
/**
|
|
147
178
|
* Try to become the SOLE monitor for this inbox. Returns true if we claimed the
|
|
148
179
|
* pidfile (caller proceeds to tail + must release it on exit); false if a LIVE
|
|
149
|
-
* holder already owns it (caller yields/exits without tailing).
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
180
|
+
* holder already owns it (caller yields/exits without tailing). The runtime
|
|
181
|
+
* calls it only while holding the modern per-inbox mutation lock, so stale
|
|
182
|
+
* reaping and successor claims are serialized. It never mutates legacy
|
|
183
|
+
* inbox-adjacent artifacts.
|
|
153
184
|
*/
|
|
154
185
|
export declare function acquireInboxLock(pidfilePath: string, ownPid: number, deps: InboxLockDeps, maxAttempts?: number, ownNonce?: string): boolean;
|
|
186
|
+
/** Legacy migration outcome. `blocked` includes stale or unreadable artifacts. */
|
|
187
|
+
export type LegacyMonitorArtifactState = 'absent' | 'live' | 'blocked';
|
|
188
|
+
export interface LegacyArtifactDeps {
|
|
189
|
+
/** Fail closed: true when the artifact exists OR cannot be inspected. */
|
|
190
|
+
exists(path: string): boolean;
|
|
191
|
+
read(path: string): string | null;
|
|
192
|
+
isAlive(pid: number): boolean;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Conservative cross-version migration boundary (gh#979): an extant legacy
|
|
196
|
+
* pidfile OR heartbeat is never replaced or unlinked by modern code. A proven
|
|
197
|
+
* live PID yields to the existing old monitor; every other artifact is a
|
|
198
|
+
* blocked migration requiring explicit operator cleanup. This avoids trying to
|
|
199
|
+
* emulate an unavailable atomic unlink-if-content primitive across binaries
|
|
200
|
+
* that do not share the modern mutation lock.
|
|
201
|
+
*/
|
|
202
|
+
export declare function legacyMonitorArtifactState(inboxPath: string, deps: LegacyArtifactDeps): LegacyMonitorArtifactState;
|
|
203
|
+
export interface ModernMonitorClaimDeps {
|
|
204
|
+
claimMutation(): boolean;
|
|
205
|
+
releaseMutation(): void;
|
|
206
|
+
legacyState(): LegacyMonitorArtifactState;
|
|
207
|
+
claimModern(): boolean;
|
|
208
|
+
releaseModern(): void;
|
|
209
|
+
}
|
|
210
|
+
export type ModernMonitorClaimResult = 'claimed' | 'mutation-busy' | 'modern-live' | 'legacy-live' | 'legacy-blocked';
|
|
211
|
+
/**
|
|
212
|
+
* Serialize every modern startup mutation for one inbox. The mutation lock is
|
|
213
|
+
* acquired BEFORE the first legacy read, spans modern lock claim, and protects
|
|
214
|
+
* the final legacy revalidation. An old binary that creates a legacy artifact
|
|
215
|
+
* at the former check→claim gap therefore makes the final check yield while
|
|
216
|
+
* preserving that artifact untouched.
|
|
217
|
+
*/
|
|
218
|
+
export declare function claimModernMonitorSafely(deps: ModernMonitorClaimDeps): ModernMonitorClaimResult;
|
|
219
|
+
export declare function defaultInboxLockDeps(): InboxLockDeps;
|
|
155
220
|
/**
|
|
156
221
|
* gh#840: read the holder heartbeat sidecar for a pidfile's inbox.
|
|
157
222
|
* Freshness = file mtime; identity = file content (the holder's nonce). Returns
|
|
@@ -161,6 +226,8 @@ export declare function readHeartbeatSidecar(pidfilePath: string): {
|
|
|
161
226
|
mtimeMs: number;
|
|
162
227
|
nonce: string;
|
|
163
228
|
} | null;
|
|
229
|
+
/** A short-lived, atomic state-root guard for all modern lock mutations. */
|
|
230
|
+
export declare function mutationLockPathFor(pidfilePath: string): string;
|
|
164
231
|
/**
|
|
165
232
|
* gh#840: write the holder heartbeat sidecar — the per-holder identity nonce as
|
|
166
233
|
* content; the FILE MTIME (touched on every write) is the freshness signal the
|
|
@@ -170,18 +237,21 @@ export declare function readHeartbeatSidecar(pidfilePath: string): {
|
|
|
170
237
|
export declare function writeHeartbeat(heartbeatPath: string, nonce: string): void;
|
|
171
238
|
/**
|
|
172
239
|
* 2026-07-02 incident: first-drone-in-a-new-cube arm race. The kickoff Monitor
|
|
173
|
-
*
|
|
174
|
-
* directory (~/.config/borgmcp/inboxes/<cubeId>/) is created by the MCP
|
|
175
|
-
* child only when the SSE stream first writes — so
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* (same constraint as assimilate-deps' mkdirp seam). The inbox FILE itself is
|
|
181
|
-
* NOT created here: it's the stream owner's; `tail -F` retries on a missing
|
|
182
|
-
* file. Exported for unit testing.
|
|
240
|
+
* pre-gh#979 `borg-inbox-monitor <inbox>` at session start, but the per-cube
|
|
241
|
+
* inbox directory (~/.config/borgmcp/inboxes/<cubeId>/) is created by the MCP
|
|
242
|
+
* server child only when the SSE stream first writes — so the legacy monitor's
|
|
243
|
+
* FIRST fs act (the pidfile-claim writeFileSync) threw ENOENT. The supported
|
|
244
|
+
* explicit-state-root mode no longer writes beside the inbox at all; this
|
|
245
|
+
* helper remains only for positional legacy compatibility. The inbox FILE is
|
|
246
|
+
* still the stream owner's; `tail -F` retries on a missing file.
|
|
183
247
|
*/
|
|
184
248
|
export declare function ensureInboxDir(inboxPath: string): void;
|
|
249
|
+
interface MonitorInvocation {
|
|
250
|
+
inboxPath: string;
|
|
251
|
+
stateRoot: string | null;
|
|
252
|
+
}
|
|
253
|
+
/** Parse the supported explicit-root command plus the legacy positional form. */
|
|
254
|
+
export declare function parseMonitorInvocation(argv: string[]): MonitorInvocation | null;
|
|
185
255
|
/**
|
|
186
256
|
* Is this module being invoked as the bin entry point?
|
|
187
257
|
*
|
|
@@ -195,4 +265,5 @@ export declare function ensureInboxDir(inboxPath: string): void;
|
|
|
195
265
|
* Exported for unit testing.
|
|
196
266
|
*/
|
|
197
267
|
export declare function isEntryInvocation(argv1: string, importMetaUrl: string): boolean;
|
|
268
|
+
export {};
|
|
198
269
|
//# sourceMappingURL=inbox-monitor.d.ts.map
|
package/dist/inbox-monitor.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as
|
|
2
|
+
import{spawn as V}from"node:child_process";import{createHash as Z,randomBytes as F}from"node:crypto";import{chmodSync as k,linkSync as tt,lstatSync as x,mkdirSync as A,readFileSync as E,realpathSync as h,statSync as H,unlinkSync as _,writeFileSync as M}from"node:fs";import{basename as B,dirname as v,isAbsolute as et,join as d,relative as rt,resolve as b,sep as ot}from"node:path";import{createInterface as nt}from"node:readline";import{fileURLToPath as it}from"node:url";const st=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,ct=1024;class at{cap;seen=new Set;order=[];constructor(e=ct){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 G(t){const e=st.exec(t);if(!e)return null;const[,,r,o,n]=e,i=n.trim();return`${r} (${o}): ${i}`}function lt(t,e){const r=G(t);return r===null?null:e.remember(t)?r:null}function ut(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let o;try{o=E(t,"utf-8")}catch(i){if(i?.code==="ENOENT")return;throw i}const n=o.split(/\r?\n/);n.at(-1)===""&&n.pop();for(const i of n.slice(-r))G(i)!==null&&e.remember(i)}function ft(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 n=e.grewSince??r,i={lastEmittedOffset:e.lastEmittedOffset,grewSince:n};return r-n>=o?{kind:"respawn",state:i}:{kind:"ok",state:i}}function P(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 mt(t,e,r){if(!e||!r.readHeartbeat)return!1;const o=r.readHeartbeat(t);if(o===null)return!1;const n=r.now?r.now():Date.now(),i=r.heartbeatStaleMs??J;return n-o.mtimeMs>=i&&o.nonce===e}function At(t){if(typeof t!="string"||t.length===0)throw new Error(`invalid monitor worktree path: ${t}`);return d(b(t),".borgmcp","inbox-monitor")}function W(t){return`${t}.monitor.pid`}function K(t){return`${t}.monitor.heartbeat`}function q(t){return Z("sha256").update(b(t)).digest("hex")}function dt(t,e){return e?d(b(e),`${q(t)}.monitor.pid`):W(t)}function pt(t,e){return e?d(b(e),`${q(t)}.monitor.heartbeat`):K(t)}function ht(t){const e=b(t);if(B(e)!=="inbox-monitor"||B(v(e))!==".borgmcp")throw new Error(`unsafe monitor state path (expected <worktree>/.borgmcp/inbox-monitor): ${e}`);const r=h(v(v(e))),o=d(r,".borgmcp"),n=d(o,"inbox-monitor");if(!z(n,r))throw new Error(`unsafe monitor state root outside worktree: ${n}`);const i=X(o);if(i&&h(o)!==o)throw new Error(`unsafe monitor state ancestor changed: ${o}`);const c=i?X(n):null;if(c&&(U(n,c),Y(d(n,".gitignore"),n)),i||j(o,448),h(o)!==o)throw new Error(`unsafe monitor state ancestor changed: ${o}`);const s=!c;s&&j(n,448);const u=h(n);if(u!==n||!z(u,r))throw new Error(`unsafe monitor state root escaped worktree: ${n}`);const m=x(u);U(u,m);const f=d(u,".gitignore"),w=`*
|
|
3
|
+
`;if(s||Y(f,u),k(u,448),s&&(M(f,w,{encoding:"utf8",mode:384}),k(f,384)),h(n)!==n)throw new Error(`unsafe monitor state root changed while preparing: ${n}`);return n}function X(t){try{const e=x(t);if(!e.isDirectory()||e.isSymbolicLink())throw new Error(`unsafe monitor state ancestor (not a real directory): ${t}`);return e}catch(e){if(e?.code==="ENOENT")return null;throw e}}function j(t,e){try{A(t,{mode:e})}catch(o){throw o?.code==="EEXIST"?new Error(`unsafe monitor state path appeared during preparation: ${t}`):o}const r=x(t);if(!r.isDirectory()||r.isSymbolicLink())throw new Error(`unsafe monitor state ancestor (not a real directory): ${t}`)}function U(t,e){if(!e.isDirectory()||e.isSymbolicLink())throw new Error(`unsafe monitor state path (not a real directory): ${t}`);if(typeof process.getuid=="function"&&e.uid!==process.getuid())throw new Error(`unsafe monitor state directory owner: ${t}`)}function Y(t,e){let r;try{r=x(t)}catch(o){throw o?.code==="ENOENT"?new Error(`unsafe monitor state root missing Borg ownership marker: ${e}`):o}if(!r.isFile()||r.isSymbolicLink())throw new Error(`unsafe monitor state ignore path: ${t}`);if(E(t,"utf8")!==`*
|
|
4
|
+
`)throw new Error(`unsafe monitor state ignore path (not Borg-owned): ${t}`);if((r.mode&511)!==384)throw new Error(`unsafe monitor state ignore path (unexpected mode): ${t}`);if(typeof process.getuid=="function"&&r.uid!==process.getuid())throw new Error(`unsafe monitor state ignore path (unexpected owner): ${t}`)}function z(t,e){const r=rt(e,t);return r!==""&&r!==".."&&!r.startsWith(`..${ot}`)&&!et(r)}const T=3e4,bt=5*T,J=5*T;function wt(t,e){return e===null?["-F","-n","0",t]:["-F","-c",`+${e+1}`,t]}function N(t){try{return H(t).size}catch{return 0}}function yt(t,e,r,o=3,n){const i=n?`${e}:${n}`:String(e);for(let c=0;c<o;c++){if(r.claim(t,i))return!0;const s=r.read(t);if(s===null)continue;const u=s.trim();if(u===""){r.removeIfContent(t,s);continue}const{pid:m,nonce:f}=P(u);if(!Number.isNaN(m)&&r.isAlive(m)){if(mt(t,f,r)){r.removeIfContent(t,s);continue}return!1}r.removeIfContent(t,s)}return!1}function gt(t,e){const r=W(t),o=K(t),n=e.exists(r),i=e.exists(o);if(!n&&!i)return"absent";if(n){const c=e.read(r);if(c!==null){const{pid:s}=P(c.trim());if(!Number.isNaN(s)&&e.isAlive(s))return"live"}}return"blocked"}function xt(t){if(!t.claimMutation())return"mutation-busy";try{const e=t.legacyState();if(e==="live")return"legacy-live";if(e==="blocked")return"legacy-blocked";if(!t.claimModern())return"modern-live";const r=t.legacyState();return r!=="absent"?(t.releaseModern(),r==="live"?"legacy-live":"legacy-blocked"):"claimed"}finally{t.releaseMutation()}}function Et(){return{claim:(t,e)=>{const r=`${t}.tmp.${process.pid}.${F(6).toString("hex")}`;try{M(r,e,{mode:384});try{return tt(r,t),!0}catch(o){if(o?.code==="EEXIST")return!1;throw o}}finally{try{_(r)}catch{}}},read:t=>{try{return E(t,"utf8")}catch{return null}},removeIfContent:(t,e)=>{try{E(t,"utf8")===e&&_(t)}catch{}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}},readHeartbeat:St,now:()=>Date.now(),heartbeatStaleMs:J}}function St(t){const e=$t(t);try{return{mtimeMs:H(e).mtimeMs,nonce:E(e,"utf8").trim()}}catch{return null}}function $t(t){return t.replace(/\.monitor\.pid$/,".monitor.heartbeat")}function It(t){return t.replace(/\.monitor\.pid$/,".monitor.mutation")}function vt(t){try{return x(t),!0}catch(e){return e?.code!=="ENOENT"}}function Q(t,e){M(t,e,{mode:384}),k(t,384)}function kt(t){A(v(t),{recursive:!0})}function Mt(t){return t.length===1&&t[0]?{inboxPath:t[0],stateRoot:null}:t.length===3&&t[0]==="--state-root"&&t[1]&&t[2]?{inboxPath:t[2],stateRoot:b(t[1])}:null}function Tt(){const t=Mt(process.argv.slice(2));t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor --state-root <worktree-runtime-root> <inbox-path>"),process.exit(2));const{inboxPath:e}=t;let r=t.stateRoot;try{r?r=ht(r):kt(e)}catch(l){const a=l instanceof Error?l.message:String(l),C=r?"runtime state directory":`inbox directory for ${e}`;console.error(`borg-inbox-monitor: cannot create ${C}: ${a}`),process.exit(1)}const o=Et(),n=dt(e,r),i=It(n),c=F(16).toString("hex"),s=`${process.pid}:${c}`,m=xt({claimMutation:()=>o.claim(i,s),releaseMutation:()=>o.removeIfContent(i,s),legacyState:()=>r?gt(e,{exists:vt,read:o.read,isAlive:o.isAlive}):"absent",claimModern:()=>yt(n,process.pid,o,3,c),releaseModern:()=>o.removeIfContent(n,`${process.pid}:${c}`)});m==="mutation-busy"&&(console.error(`borg-inbox-monitor: another modern monitor startup is mutating this inbox state; re-arm after it finishes (if it persists after that process stops, confirm it is stopped, remove ${i}, then re-arm)`),process.exit(1)),m==="legacy-live"&&process.exit(0),m==="legacy-blocked"&&(console.error(`borg-inbox-monitor: legacy monitor artifact remains beside ${e}; stop/confirm the old monitor, remove its .monitor.pid/.monitor.heartbeat manually, then re-arm`),process.exit(1)),m==="modern-live"&&process.exit(0);const f=()=>{if(o.claim(i,s))try{o.removeIfContent(n,`${process.pid}:${c}`)}finally{o.removeIfContent(i,s)}},w=pt(e,r),S=()=>{if(o.claim(i,s))try{o.removeIfContent(w,c)}finally{o.removeIfContent(i,s)}};try{Q(w,c)}catch(l){f();const a=l instanceof Error?l.message:String(l);console.error(`borg-inbox-monitor: cannot write runtime heartbeat: ${a}`),process.exit(1)}const O=new at;ut(e,O);let p={lastEmittedOffset:N(e),grewSince:null},$=!1,y=null;const R=l=>{const a=V("tail",wt(e,l),{stdio:["ignore","pipe","inherit"]});y=a,a.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),S(),f(),process.exit(1)),nt({input:a.stdout,crlfDelay:1/0}).on("line",g=>{const I=lt(g,O);I!==null&&(console.log(I),p={lastEmittedOffset:N(e),grewSince:null})}),a.on("error",g=>{a===y&&(console.error(`borg-inbox-monitor: tail failed: ${g.message}`),S(),f(),process.exit(1))}),a.on("exit",(g,I)=>{a===y&&(S(),f(),$&&process.exit(0),I&&process.exit(0),process.exit(g??0))})};R(null);const D=setInterval(()=>{try{Q(w,c)}catch{}const l=ft(N(e),p,Date.now(),bt);if(p=l.state,l.kind==="respawn"&&!$){const a=y;R(p.lastEmittedOffset);try{a?.kill("SIGKILL")}catch{}p={lastEmittedOffset:p.lastEmittedOffset,grewSince:null}}},T);D.unref();const L=l=>{if($)return;$=!0,clearInterval(D),S(),f();const a=y;a&&!a.killed&&!a.kill(l)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref()};process.once("SIGTERM",()=>L("SIGTERM")),process.once("SIGINT",()=>L("SIGINT"))}function Nt(t,e){try{return h(t)===it(e)}catch{return!1}}Nt(process.argv[1],import.meta.url)&&Tt();export{J as HEARTBEAT_STALE_MS,ct as RECENT_EMITTED_LINE_CAP,at as RecentLineDeduper,yt as acquireInboxLock,xt as claimModernMonitorSafely,Et as defaultInboxLockDeps,kt as ensureInboxDir,ht as ensureMonitorStateDir,ft as evaluateInboxTailStall,G as formatEventLine,lt as formatFreshEventLine,pt as heartbeatPathFor,Nt as isEntryInvocation,mt as isHolderWedged,K as legacyHeartbeatPathFor,gt as legacyMonitorArtifactState,W as legacyPidfilePathFor,At as monitorStateRootForWorktree,It as mutationLockPathFor,Mt as parseMonitorInvocation,P as parsePidfileContent,dt as pidfilePathFor,St as readHeartbeatSidecar,ut as seedDeduperFromInboxTail,wt as tailArgsFor,Q as writeHeartbeat};
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Server as
|
|
2
|
+
import{Server as X}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Z}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as ee,ListToolsRequestSchema as te,ListPromptsRequestSchema as oe,GetPromptRequestSchema as re}from"@modelcontextprotocol/sdk/types.js";import{assertRoleMatches as ne}from"./role-match.js";import{getCubeInfo as se,getRoleInfo as A,getRoleInfoByName as ie,getRoster as ae,readLog as ce,appendLog as le,submitReport as de,fetchReports as ue,ackLogEntry as pe,recordDecision as me,listDecisions as be,regen as L,listCubes as fe,createCube as ge,updateCube as O,deleteCube as he,createRole as _e,updateRole as ye,patchRoleSection as E,patchTaxonomyClass as B,deleteRole as we,reassignDrone as xe,evictDrone as $e,getCube as w,checkSubscriptionStatus as ve,createBillingPortalSession as ke,createSubscription as Ee,syncRoles as Ce,applyTemplate as Re,whoami as Se,roleRationale as Ie,getValidToken as Te}from"./remote-client.js";import{startHealthBeatTick as qe}from"./health-beat.js";import{getTemplate as C,listTemplateNames as R,resolveCubeDirectiveForCreate as De,resolveCubeDirectiveForApply as Pe,resolveMessageTaxonomyForCreate as Ue}from"./templates.js";import{activeCubeWithFreshRegenIdentity as F,getActiveCube as h,setActiveCube as H,findProjectRoot as S,inboxPathForDrone as I}from"./cubes.js";import{monitorStateRootForWorktree as T}from"./inbox-monitor.js";import{addSessionStartHook as je,addUserPromptSubmitHook as Ne}from"./config-utils.js";import{humanAgo as W,formatLogEntryMarkdown as Me,formatRegenMarkdown as V,getDronePlaybook as Ae,getDronePlaybookChapter as Le,nullTaxonomyTip as Oe,regenWakePathDroneLabel as Be}from"./regen-format.js";import{startLogStream as Fe,getStreamStatus as q}from"./log-stream.js";import{isMcpReadinessProbe as He}from"./readiness-probe.js";import{runMcpStartupServices as We}from"./startup-services.js";import{TOOL_MANIFEST as Ve}from"./tool-manifest.js";import{DOCS_SECTIONS as ze,matchDocsSections as Je,formatDocsIndex as Qe}from"./docs-sections.js";import{renderRoleList as Ye}from"./list-roles-render.js";import{filterToolsForRole as Ge}from"./tool-scope.js";import{getPackageVersion as x,getOnDiskVersion as Ke,handleVersionFlag as Xe}from"./version.js";import{renderStreamStatus as Ze,checkInboxMonitorHealthy as D,formatWakePathPrefix as et,shouldShowWakePathWarning as tt}from"./stream-status.js";import{formatRoleAgentLabel as ot,renderRoster as rt}from"./roster-render.js";import{resolveDroneIdByLabel as nt,isUuidShape as st}from"./evict-drone.js";import{authRecoveryMessage as it}from"./auth-recovery.js";import{DroneEvictedError as at,DroneFrozenError as ct,formatEvictedToolResult as lt,formatFrozenToolResult as dt}from"./drone-lifecycle.js";import{classifyInSessionAssimilate as ut,reattachOnlyRefusal as pt,reattachFailureMessage as mt}from"./assimilate-guard.js";import{gateAllowsActivation as bt,borgSessionToolNotice as ft}from"./launch-gate.js";import{renderSyncRolesResult as gt}from"./sync-roles-render.js";import{initConsolePrefix as ht,consolePrefix as $}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as P,resolveSessionAgentKind as U,probeCodexBridgeArmed as _t}from"./codex-app-wake.js";import{connectOpenCodeDrone as yt,injectOpenCodeEntry as wt,probeOpenCodeDroneArmed as xt,computeOpenCodePort as $t}from"./opencode-drone.js";import{installBorgPlugin as vt}from"./opencode-plugin.js";import{setModuleInjectOpenCode as kt}from"./log-stream.js";import{lifecycleSignalForMessage as Et,recordLifecycleLog as z,shouldSuppressLifecycleLog as Ct}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Rt}from"./direct-log.js";import J from"open";import St from"os";function It(){try{const m=St.hostname();return m&&m.trim()?m.trim().slice(0,255):null}catch{return null}}async function Q(m,y){return await Re(m,y.name)}async function _(){const m=await h();if(!m)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");return m}async function Tt(){Xe();const m=He();await We(m,{sessionStartHook:()=>{je()},auditHook:()=>{Ne()},sseStream:()=>{Fe()},openCode:async()=>{vt();const l=await h();if(l&&process.env.BORG_OPENCODE==="1"){const o=`http://127.0.0.1:${$t(l.droneId)}`;await yt({serverUrl:o,directory:process.cwd(),droneLabel:l.droneLabel,cubeName:l.name}),kt(wt)}},healthBeat:()=>{qe({getActiveCube:h,getStreamConnected:()=>q().connected,getInboxPath:l=>I(l.cubeId,l.droneId),checkMonitor:l=>D(l,T(S())),isCodexRemoteWake:P,probeBridgeArmed:l=>_t({cubeId:l.cubeId,droneId:l.droneId}),probeOpenCodeDrone:()=>xt(),resolveAgentKind:U,resolveHostname:It,resolveVersion:x,getToken:Te,fetchImpl:globalThis.fetch.bind(globalThis)})}});const y=new X({name:"borg-mcp-client",version:x()},{capabilities:{tools:{},prompts:{}}}),j=Ve;y.setRequestHandler(te,async()=>{let l=null;try{const p=await h();p&&(l={roleName:p.roleName,roleClass:p.roleClass,isHumanSeat:p.isHumanSeat})}catch{l=null}return{tools:Ge(j,l)}}),y.setRequestHandler(ee,async l=>{let{name:p,arguments:o}=l.params;if(p==="borg_describe-tool"){const e=typeof o?.name=="string"?o.name:"",t=j.find(r=>r.name===e);return t?{content:[{type:"text",text:JSON.stringify({name:t.name,description:t.description,inputSchema:t.inputSchema},null,2)}]}:{content:[{type:"text",text:`Unknown borg tool: ${e||"(none)"}. Pass { name: "<borg_tool>" }.`}],isError:!0}}if(p==="borg_tool"){const e=typeof o?.name=="string"?o.name:"";if(!e||e==="borg_tool"||e==="borg_describe-tool")return{content:[{type:"text",text:'borg_tool: pass { name: "<borg_tool>", arguments: {...} } naming a real borg tool (not the dispatcher itself).'}],isError:!0};o=o?.arguments&&typeof o.arguments=="object"?o.arguments:{},p=e}if(!bt(`tool ${p}`))return{content:[{type:"text",text:ft(p)}],isError:!0};try{switch(p){case"borg_regen":{const e=await h();if(!e)return{content:[{type:"text",text:'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.'}]};const t=typeof o?.since=="string"?o.since:void 0,r=o?.mode==="lite"?"lite":"full",n=await L(e.sessionToken,e.apiUrl,{since:t}),s=F(e,n);s!==e&&await H(s);const i=q(),a=I(s.cubeId,s.droneId),c=T(S()),u=U()==="opencode"||P()?!0:D(a,c),f=tt(i,u)?et({inboxPath:a,monitorStateRoot:c,droneLabel:Be(n,s.droneLabel),cubeName:s.name}):"";let g="";try{const d=x(),b=Ke();if(d!=="unknown"&&b!=="unknown"&&b!==d){const[v,N,G]=d.split(".").map(Number),[k,M,K]=b.split(".").map(Number);(k>v||k===v&&M>N||k===v&&M===N&&K>G)&&(g=`## \u{1F504} borgmcp ${b} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${d}.
|
|
3
3
|
|
|
4
|
-
`)}}catch{}return{content:[{type:"text",text:
|
|
5
|
-
`)+
|
|
4
|
+
`)}}catch{}return{content:[{type:"text",text:g+f+V(n,{mode:r})}]}}case"borg_subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await Ee()}`}]};case"borg_upgrade-subscription":{const e=await ke();try{await J(e)}catch{}return{content:[{type:"text",text:`Manage your Borg MCP subscription at: ${e}`}]}}case"borg_subscription_status":{const e=await ve();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"borg_open_dashboard":{const e="https://borgmcp.ai/dashboard";return await J(e),{content:[{type:"text",text:`\u25FC Opened dashboard in browser: ${e}`}]}}case"borg_assimilate":{const e=o?.cube_name;if(!e)throw new Error("cube_name is required");const t=await h(),r=ut(t,e);if(r.kind!=="reattach")return{content:[{type:"text",text:pt(r,e)}],isError:!0};try{const n=await L(t.sessionToken,t.apiUrl,{}),s=F(t,n);return s!==t&&await H(s),{content:[{type:"text",text:[`# Re-attached to cube: ${s.name}`,"",`**Drone label:** ${s.droneLabel}`,"**Seat:** existing identity reused \u2014 no new drone minted (gh#780)","",""].join(`
|
|
5
|
+
`)+V(n,{mode:"full"})}]}}catch(n){const s=mt(n??{});if(!s)throw n;return{content:[{type:"text",text:s}],isError:!0}}}case"borg_version":return{content:[{type:"text",text:`borgmcp ${x()}`}]};case"borg_playbook":return{content:[{type:"text",text:Le()}]};case"borg_docs":{const e=typeof o?.topic=="string"?o.topic.trim():"",t=e?Je(e):[],r=t.length>0?t:ze;return{content:[{type:"text",text:`${e&&t.length>0?`Best-matching docs section(s) for "${e}" \u2014 WebFetch the URL for the full page:`:e?`No exact match for "${e}". Full Borg MCP docs index \u2014 WebFetch the URL you need:`:"Borg MCP docs index \u2014 WebFetch the URL of the section you need:"}
|
|
6
6
|
|
|
7
|
-
${
|
|
8
|
-
`)}]}}case"borg_role":{const e=await _(),t=typeof
|
|
9
|
-
`)}]}}const{role:
|
|
10
|
-
`)}]}}case"borg_role-rationale":{const e=await _(),t=typeof
|
|
11
|
-
`)}]}}case"borg_roster":{const e=await _(),t=typeof
|
|
7
|
+
${Qe(r)}`}]}}case"borg_whoami":{const e=await _(),t=await Se(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg_cube":{const e=await _(),[{cube:t,roles:r}]=await Promise.all([se(e.sessionToken,e.apiUrl),A(e.sessionToken,e.apiUrl)]),n=[];n.push(`# Cube: ${t.name}`),n.push(""),n.push("## Cube directive"),n.push(t.cube_directive||"_(none)_"),n.push("");const s=Oe(t.message_taxonomy);if(s&&(n.push(s),n.push("")),n.push("## Roles in this cube"),!r.length)n.push("_(no roles defined)_");else{for(const i of r){const a=[i.role_class==="queen"?"Queen":null,i.is_human_seat?"human-seat":null,i.is_default?"default":null].filter(Boolean).join(", "),c=a?` (${a})`:"",u=i.short_description||"_(no description)_";n.push(`- **${i.name}**${c} \u2014 ${u}`)}n.push(""),n.push("_(Coordinator-class drones can fetch role IDs via `borg_list-roles` for use with `borg_reassign-drone`.)_")}return n.push(""),n.push(Ae()),{content:[{type:"text",text:n.join(`
|
|
8
|
+
`)}]}}case"borg_role":{const e=await _(),t=typeof o?.role=="string"?o.role.trim():"";if(t){const{role:s}=await ie(e.sessionToken,e.apiUrl,t);return ne(t,s),{content:[{type:"text",text:[`# Role: ${s.name}`,"",s.detailed_description||"_(no detailed description set)_"].join(`
|
|
9
|
+
`)}]}}const{role:r}=await A(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:[`# Your role: ${r.name}`,"",r.detailed_description||"_(no detailed description set)_"].join(`
|
|
10
|
+
`)}]}}case"borg_role-rationale":{const e=await _(),t=typeof o?.role=="string"?o.role:"",r=typeof o?.section=="string"?o.section:"",n=await Ie(e.sessionToken,e.apiUrl,t,r);return{content:[{type:"text",text:[`# Role rationale: ${n.role} \u2014 ${n.section}`,"",n.body||"_(empty)_"].join(`
|
|
11
|
+
`)}]}}case"borg_roster":{const e=await _(),t=typeof o?.since=="string"?o.since:void 0,{drones:r,roles:n,since:s}=await ae(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:rt({cubeName:e.name,drones:r,roles:n,resolvedSince:s??null,humanAgo:W})}]}}case"borg_stream-status":{const e=q(),t=await h(),r=t?I(t.cubeId,t.droneId):null,n=t?T(S()):null,s=t&&U()==="opencode",i=t?s||P()?!0:D(r,n):null;let a="";e.runLoopHealth==="silent-inert"&&(a=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
|
|
12
12
|
|
|
13
13
|
The log-stream consumer started but never connected. This drone will not receive real-time cube events.
|
|
14
14
|
|
|
15
|
-
`);const
|
|
16
|
-
`)}]}}case"borg_log":{const e=
|
|
17
|
-
${
|
|
18
|
-
\u26A0 ${
|
|
19
|
-
${
|
|
15
|
+
`);const c=Ze({status:e,inboxMonitorHealthy:i,inboxPath:r,monitorStateRoot:n,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:W});return{content:[{type:"text",text:a+c}]}}case"borg_read-log":{const e=await _(),t=typeof o?.since=="string"?o.since:void 0,r=typeof o?.limit=="number"?o.limit:void 0,n=o?.unread_only===!0||o?.unread_only==="true",{entries:s,drones:i,roles:a,behind_by:c,has_more:u}=await ce(e.sessionToken,e.apiUrl,{since:t,limit:r,unreadOnly:n}),f=new Map;for(const b of i)f.set(b.id,b);const g=new Map;for(const b of a)g.set(b.id,b);const d=[];if(d.push(`# Activity log: ${e.name}`),d.push(""),!s.length)d.push("_(no entries)_");else for(const b of s)d.push(Me(b,f,g));return u===!0?(d.push(""),d.push("\u26A0 has_more: true \u2014 call `borg_read-log unread_only=true` again until has_more=false so you finish draining unread entries.")):typeof c=="number"&&c>0&&(d.push(""),d.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg_read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:d.join(`
|
|
16
|
+
`)}]}}case"borg_log":{const e=o?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await h();if(!t)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");if(Et(e)){const d=await Ct(t,e);if(d.suppress)return await z(t,e),{content:[{type:"text",text:`Suppressed duplicate ${d.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const r=Object.prototype.hasOwnProperty.call(o??{},"to"),n=r?Rt(o?.to):void 0,s=typeof o?.class=="string"?o.class:void 0,i=o?.visibility==="broadcast"||o?.visibility==="direct"?o.visibility:void 0,a={...s?{class:s}:{},...r?{to:n??[]}:{},...i?{visibility:i}:{}},c=await le(t.sessionToken,t.apiUrl,e,a);await z(t,e);const u=c.routing?.message?`
|
|
17
|
+
${c.routing.message}`:"",f=c.unreachableRecipients?.length?`
|
|
18
|
+
\u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(d=>d.label).join(", ")}. Message delivered \u2014 they'll read it when they return.`:"";return{content:[{type:"text",text:`Logged to cube "${t.name}" as ${t.droneLabel}. (entry id: ${c.entry.id})${u}${f}`}]}}case"borg_report-friction":{const e=o?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await h();if(!t)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");const r=o?.kind==="bug"?"bug":"friction",n=o?.metadata&&typeof o.metadata=="object"&&!Array.isArray(o.metadata)?o.metadata:void 0;return{content:[{type:"text",text:(await de(t.sessionToken,t.apiUrl,{kind:r,message:e,metadata:n})).ok?"Report submitted \u2014 thank you. The borgmcp team will see it. (Write-only: you cannot read reports back.)":"Report did not submit. Try again, or raise it in the cube log."}]}}case"borg_reports":{const e=await ue();if(e.forbidden)return{content:[{type:"text",text:"Reports triage is builder/dogfooder-tier only. Your account is not on the dogfooder (builder) tier, so the friction-reports store is not readable. (Server-enforced gate.)"}]};if(!e.reports.length)return{content:[{type:"text",text:"No reports yet. Submissions via borg_report-friction will appear here, newest first."}]};const t=e.reports.map(r=>{const n=r.metadata&&Object.keys(r.metadata).length?" \xB7 "+Object.entries(r.metadata).map(([i,a])=>`${i}=${a}`).join(", "):"",s=r.redacted?" \xB7 [secrets-scrubbed]":"";return`**[${r.kind}]** ${r.created_at} \xB7 ${r.reporter_email}${n}${s}
|
|
19
|
+
${r.message}`});return{content:[{type:"text",text:`Reports (${e.reports.length}, newest first):
|
|
20
20
|
|
|
21
21
|
${t.join(`
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
`)}`}]}}case"borg_ack":{const e=
|
|
26
|
-
`)}]}}case"borg_list-cubes":{const{cubes:e}=await
|
|
27
|
-
${(
|
|
25
|
+
`)}`}]}}case"borg_ack":{const e=o?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=o?.kind==="claim"?"claim":"ack",r=await _();return await pe(r.sessionToken,r.apiUrl,e,t),{content:[{type:"text",text:t==="claim"?`Claimed entry ${e} in cube "${r.name}" (advisory \u2014 merge stays keyed on REVIEW-APPROVED).`:`Acked entry ${e} in cube "${r.name}".`}]}}case"borg_decide":{const e=o?.topic,t=o?.decision;if(!e||typeof e!="string")throw new Error("topic is required");if(!t||typeof t!="string")throw new Error("decision is required");const r=typeof o?.rationale=="string"?o.rationale:void 0,n=await _(),{decision:s}=await me(n.sessionToken,n.apiUrl,{topic:e,decision:t,...r!==void 0?{rationale:r}:{}}),i=s?.supersedes?" (superseded the prior decision on this topic)":"";return{content:[{type:"text",text:`Recorded ratified decision on "${e}" in cube "${n.name}"${i}. Cite it via borg_decisions; it surfaces in borg_regen.`}]}}case"borg_decisions":{const e=typeof o?.topic=="string"?o.topic:void 0,t=await _(),{decisions:r}=await be(t.sessionToken,t.apiUrl,e);return{content:[{type:"text",text:r.length===0?e?`No active ratified decision on "${e}" in cube "${t.name}".`:`No active ratified decisions in cube "${t.name}".`:r.map(s=>`**${s.topic}:** ${s.decision}${s.rationale?` \u2014 ${s.rationale}`:""}`).join(`
|
|
26
|
+
`)}]}}case"borg_list-cubes":{const{cubes:e}=await fe();if(!e.length)return{content:[{type:"text",text:"No cubes yet. Use borg_create-cube to make your first one."}]};const t=e.map(r=>`- **${r.name}** (id: ${r.id})
|
|
27
|
+
${(r.cube_directive||"_(no directive set)_").split(`
|
|
28
28
|
`)[0].slice(0,120)}`);return{content:[{type:"text",text:`Your cubes (${e.length}):
|
|
29
29
|
|
|
30
30
|
${t.join(`
|
|
31
31
|
|
|
32
|
-
`)}`}]}}case"borg_create-cube":{const e=
|
|
32
|
+
`)}`}]}}case"borg_create-cube":{const e=o?.name,t=o?.cube_directive,r=o?.template;if(!e)throw new Error("name is required");if(t===void 0)throw new Error("cube_directive is required (pass empty string if none)");let n=null;if(r&&(n=C(r),!n))throw new Error(`Unknown template "${r}". Available: ${R().join(", ")}`);const s=De(t,n),i=Ue(void 0,n),a=await ge(e,s,{message_taxonomy:i});if(n){const u=await Q(a.id,n),f=s!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${r}** applied \u2014 ${u.created} role(s) created, ${u.updated} updated.${f} Use borg_assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${a.name} to join as a drone.`}]}}case"borg_update-cube":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof o?.name=="string"&&(t.name=o.name),typeof o?.cube_directive=="string"&&(t.cube_directive=o.cube_directive),Array.isArray(o?.message_taxonomy)&&(t.message_taxonomy=o.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:r}=await O(e,t);return{content:[{type:"text",text:`Updated cube **${r.name}** (id: ${r.id}).`}]}}case"borg_patch-taxonomy-class":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const t=o?.action;if(t!=="add"&&t!=="replace"&&t!=="remove")throw new Error("action must be one of: add, replace, remove.");let r,n;if(t==="remove"){const i=o?.class;if(!i)throw new Error("class is required for remove.");({cube:r}=await B(e,{action:t,class:i})),n=i}else{const i=o?.class_def;if(i==null||typeof i!="object"||Array.isArray(i))throw new Error("class_def (object) is required for add/replace.");({cube:r}=await B(e,{action:t,class_def:i})),n=String(i.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${n}** in cube **${r.name}** (id: ${r.id}).`}]}}case"borg_delete-cube":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");return await he(e),{content:[{type:"text",text:`Deleted cube ${e} (and all its roles, drones, log entries).`}]}}case"borg_create-role":{const e=o?.cube_id,t=o?.name,r=o?.short_description,n=o?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(r===void 0)throw new Error("short_description is required (pass empty string if none)");if(n===void 0)throw new Error("detailed_description is required (pass empty string if none)");const s=o?.is_default===!0,i=o?.is_human_seat===!0,a=o?.can_broadcast===!0,c=o?.receives_all_direct===!0,{role:u}=await _e(e,{name:t,short_description:r,detailed_description:n,is_default:s,is_human_seat:i,can_broadcast:a,receives_all_direct:c,...typeof o?.default_model=="string"?{default_model:o.default_model}:{}}),f=[u.role_class==="queen"?"Queen":null,u.is_human_seat?"human-seat":null,u.is_default?"default":null].filter(Boolean).join(", "),g=f?` (${f})`:"";return{content:[{type:"text",text:`Created role **${u.name}**${g} (id: ${u.id}) in cube ${e}.`}]}}case"borg_update-role":{const e=o?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof o?.name=="string"&&(t.name=o.name),typeof o?.short_description=="string"&&(t.short_description=o.short_description),typeof o?.detailed_description=="string"&&(t.detailed_description=o.detailed_description),typeof o?.is_default=="boolean"&&(t.is_default=o.is_default),typeof o?.is_human_seat=="boolean"&&(t.is_human_seat=o.is_human_seat),typeof o?.can_broadcast=="boolean"&&(t.can_broadcast=o.can_broadcast),typeof o?.receives_all_direct=="boolean"&&(t.receives_all_direct=o.receives_all_direct),typeof o?.default_model=="string"&&(t.default_model=o.default_model),Object.keys(t).length===0)throw new Error("Pass at least one of: name, short_description, detailed_description, is_default, is_human_seat, can_broadcast, receives_all_direct, default_model.");const{role:r}=await ye(e,t),n=[r.role_class==="queen"?"Queen":null,r.is_human_seat?"human-seat":null,r.is_default?"default":null].filter(Boolean).join(", "),s=n?` (${n})`:"";return{content:[{type:"text",text:`Updated role **${r.name}**${s} (id: ${r.id}).`}]}}case"borg_patch-role-section":{const e=o?.role_id;if(!e)throw new Error("role_id is required");const t=o?.action;if(t!=="replace"&&t!=="insert"&&t!=="delete")throw new Error("action must be one of: replace, insert, delete.");const r=o?.heading;if(!r)throw new Error("heading is required");let n;if(t==="delete")({role:n}=await E(e,{action:t,heading:r}));else{const i=o?.body;if(typeof i!="string")throw new Error("body is required for replace/insert (pass empty string for an empty section).");if(t==="insert"){const a=typeof o?.after=="string"?o.after:null;({role:n}=await E(e,{action:t,heading:r,body:i,after:a}))}else({role:n}=await E(e,{action:t,heading:r,body:i}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${r}** in role **${n.name}** (id: ${n.id}).`}]}}case"borg_delete-role":{const e=o?.role_id;if(!e)throw new Error("role_id is required");return await we(e),{content:[{type:"text",text:`Deleted role ${e}.`}]}}case"borg_reassign-drone":{const e=o?.drone_id,t=o?.role_id;if(!e)throw new Error("drone_id is required");if(!t)throw new Error("role_id is required");const{drone:r}=await xe(e,t);return{content:[{type:"text",text:`Reassigned drone ${r.label} (${r.id}) to role ${r.role_id}.`}]}}case"borg_evict-drone":{const e=o?.drone_id?.trim(),t=o?.label?.trim(),r=o?.cube_id?.trim();let n,s;if(e){if(!st(e))throw new Error(`drone_id "${e}" is not a UUID \u2014 if that's a drone label, pass it as label + cube_id instead.`);n=e,s=e}else if(t){if(!r)throw new Error("cube_id is required when evicting by label");const{drones:i}=await w(r),a=nt(i,t);if(!a)throw new Error(`No active drone labelled "${t}" in cube ${r} (it may already be evicted; check borg_list-drones).`);n=a.id,s=a.label}else throw new Error("Provide drone_id, or label + cube_id, to identify the drone to evict");return await $e(n),{content:[{type:"text",text:`Evicted drone ${s} (${n}). Soft-deleted: removed from the roster and freed its seat; log history preserved with anonymized attribution.`}]}}case"borg_list-drones":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:r}=await w(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const n=new Map(r.map(i=>[i.id,i])),s=t.map(i=>{const a=n.get(i.role_id),c=ot(a?.name??"?",i.agent_kind),u=i.wake_path_alert_class&&i.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${i.wake_path_alert_class}`:"";return`- **${i.label}** (id: ${i.id}) \u2014 role: ${c} (${i.role_id}) \u2014 last seen ${i.last_seen}${u}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
|
|
33
33
|
|
|
34
34
|
${s.join(`
|
|
35
|
-
`)}`}]}}case"borg_list-roles":{const e=
|
|
35
|
+
`)}`}]}}case"borg_list-roles":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await w(e);return{content:[{type:"text",text:Ye(t,e)}]}}case"borg_list-templates":return{content:[{type:"text",text:`Available templates:
|
|
36
36
|
|
|
37
|
-
${
|
|
38
|
-
`)}`}]};case"borg_sync-roles":{const e=
|
|
37
|
+
${R().map(r=>{const n=C(r);return`- **${r}**: ${n.description}`}).join(`
|
|
38
|
+
`)}`}]};case"borg_sync-roles":{const e=o?.cube_id,t=o?.template_name||"software-dev",r=o?.apply===!0,n=o?.decisions&&typeof o.decisions=="object"?o.decisions:void 0;if(!e)throw new Error("cube_id is required");const s=await Ce(e,t,r,n);return{content:[{type:"text",text:gt(s,t)}]}}case"borg_apply-template":{const e=o?.cube_id,t=o?.template_name;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("template_name is required");const r=C(t);if(!r)throw new Error(`Unknown template "${t}". Available: ${R().join(", ")}`);const n=await Q(e,r);let s="";const i=await w(e),a=Pe(i.cube_directive,r);return a!==null&&(await O(e,{cube_directive:a}),s=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${n.created} role(s) created, ${n.updated} updated.${s}`}]}}default:throw new Error(`Unknown tool: ${p}`)}}catch(e){if(e instanceof at)return{content:[{type:"text",text:lt(e.message)}],isError:!0};if(e instanceof ct)return{content:[{type:"text",text:dt(e.message)}],isError:!0};const t=it(e??{});return t?{content:[{type:"text",text:t}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),y.setRequestHandler(oe,async()=>({prompts:[{name:"borg_subscribe",description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),y.setRequestHandler(re,async l=>{const{name:p}=l.params;switch(p){case"borg_subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial.",messages:[{role:"user",content:{type:"text",text:"Please help me set up a Borg MCP subscription using the subscribe tool."}}]};case"dashboard":return{description:"Open Borg MCP dashboard to manage cubes",messages:[{role:"user",content:{type:"text",text:"Please open the Borg MCP dashboard using the borg_open_dashboard tool."}}]};default:throw new Error(`Unknown prompt: ${p}`)}});const Y=new Z;await y.connect(Y),await ht(),console.error(`${$()}\u25FC Borg MCP Client started`),console.error(`${$()}\u25FC Use borg_assimilate <cube-name> to join a cube as a drone`),console.error(`${$()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}Tt().catch(m=>{console.error(`${$()}Fatal error:`,m),process.exit(1)});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Environment contract for the short-lived `borg assimilate` MCP readiness child. */
|
|
2
|
+
export declare const MCP_READINESS_PROBE_ENV = "BORG_MCP_READINESS_PROBE";
|
|
3
|
+
export declare function isMcpReadinessProbe(env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
export declare function readinessProbeEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
5
|
+
//# sourceMappingURL=readiness-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n="BORG_MCP_READINESS_PROBE";function r(e=process.env){return e[n]==="1"}function o(e=process.env){return{...e,[n]:"1"}}export{n as MCP_READINESS_PROBE_ENV,r as isMcpReadinessProbe,o as readinessProbeEnv};
|
package/dist/regen-format.d.ts
CHANGED
|
@@ -41,10 +41,11 @@ export type AgentKind = 'claude' | 'codex' | 'opencode';
|
|
|
41
41
|
* be wrong; the recovery is a manual `borg_regen` on return.
|
|
42
42
|
*
|
|
43
43
|
* `inboxPath` is the deterministic client-generated UUID path
|
|
44
|
-
* (`~/.config/borgmcp/inboxes/<cubeId>/<droneId>.log`)
|
|
45
|
-
*
|
|
44
|
+
* (`~/.config/borgmcp/inboxes/<cubeId>/<droneId>.log`), while the optional
|
|
45
|
+
* explicit state root is derived from the saved worktree path. Both are
|
|
46
|
+
* shell-escaped before rendering the launch/orientation command.
|
|
46
47
|
*/
|
|
47
|
-
export declare function wakePathArming(agentKind: AgentKind, inboxPath: string): string;
|
|
48
|
+
export declare function wakePathArming(agentKind: AgentKind, inboxPath: string, monitorStateRoot?: string | null): string;
|
|
48
49
|
/**
|
|
49
50
|
* Resolve the lean-orientation identity (gh#927), preferring the fresh
|
|
50
51
|
* network `regen()` result and falling back per-field to the local
|
|
@@ -95,6 +96,8 @@ export declare function formatLeanOrientation(args: {
|
|
|
95
96
|
droneLabel: string;
|
|
96
97
|
roleName?: string | null;
|
|
97
98
|
inboxPath: string;
|
|
99
|
+
/** Explicit worktree-local root for Claude monitor PID/heartbeat state. */
|
|
100
|
+
monitorStateRoot?: string | null;
|
|
98
101
|
agentKind: AgentKind;
|
|
99
102
|
source?: string | null;
|
|
100
103
|
}): string;
|
package/dist/regen-format.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{ROLE_SCOPED_SAFETY_DISCIPLINES as
|
|
2
|
-
`)}function
|
|
3
|
-
`)}function x(){return"## How to operate as a Drone\n\nYou're a Drone in a Cube. Coordinate with other drones through the activity log.\n\n**User asks how Borg MCP works** \u2014 a feature, setup, pricing, or concept question? Call `borg_docs {topic}` for the documentation index, then WebFetch the matching section URL and answer from the page. Don't guess borgmcp's own behavior from memory.\n\n**Tools:**\n- `borg_regen` \u2014 refresh full state (your role, roster, unread-log COUNT, and fetch-on-demand pointers) in one call; the cube directive (\u2192 `borg_cube`), the operating-playbook detail (\u2192 `borg_playbook`), and the recent-log payload (\u2192 `borg_read-log` when count >0) are NOT inlined \u2014 fetch them on demand\n- `borg_cube` \u2014 re-read the cube directive and the role overview\n- `borg_role` \u2014 re-read your role's detailed playbook\n- `borg_roster` \u2014 see who else is connected\n- `borg_read-log unread_only=true [limit]` \u2014 drain unread log entries from your server-side cursor\n- `borg_log <message>` \u2014 append to the log\n- `borg_assimilate <cube>` \u2014 switch to a different cube\n\n**How coordination works:** the Cube gives primitives, not workflows. Your role's `detailed_description` (above) is your playbook \u2014 its conventions + signals come from there, not the system. The log is the coordination channel. Different cubes, different conventions.\n\n**Default: act autonomously, coordinate through the log.** Don't wait for user input. Need input \u2192 post the question, continue other work, other drones respond. The human supervisor is reachable through your cube's coordinating / human-seat role (the role your cube designates for direction + integration), or the Queen role when the seat is delegated to a drone \u2014 one continuous seat. Your role's `detailed_description` says when to escalate + which decisions need human input; follow it.\n\n**Operating loop \u2014 each wake, in order:**\n1. Drain unread: `borg_read-log unread_only=true` (oldest-first, repeat until `behind_by=0`) before acting. The \"Cube log\" section gives your UNREAD COUNT.\n2. Apply your role's conventions to each entry. Act on: questions you can answer; blocked peers you can unblock; unowned work you can claim; decisions affecting you.\n3. Actionable signal \u2192 act + post the convention. Don't wait to be asked.\n4. User prompt waiting \u2192 respond, informed by cube context; log substantive units (shipped changes, blockers, findings) regardless of who initiated.\n5. Nothing actionable + no prompt \u2192 done; wait for next wake.\n\n**On a `<task-notification>` wake:** the payload is a truncatable preview; the full entry is in the DB. Drain: `borg_read-log unread_only=true limit=20`, repeat until `behind_by=0`. Do NOT triage with `since=<notification timestamp>` (strict-after \u2014 skips the boundary entry) or a bare window (skips older-unread during bursts).\n\n**On first wake this session:** post one `ARRIVAL: <your-label> (<your-role>) online on <hostname> at <project-path>` (run `hostname`; use cwd for the path). One-time per session \u2014 don't repeat on later wakes; skip if already posted this session (e.g. after a `/mcp` reconnect).\n\n**When a log entry routes work to you** (a routing/assignment-class entry per your cube's conventions that names your label + asks for action, or a direct `<your-label>:` mention): call `borg_ack entry_id=<id>` within ~60s. Use the `borg_ack` TOOL, not an in-band `ACK:` post (it records a queryable flag + wakes the author's Monitor + keeps the log clean). Ack = receipt, not completion (`STARTING` / `DONE` still apply). Ack only routing-class signals \u2014 not every mention.\n\n**Claim a work item before you start it (`borg_ack ... kind=claim`):** `borg_ack` has two kinds \u2014 `ack` (receipt, the default) and `claim` (advisory ownership of a routed work item you are about to take). When a routed entry could be picked up by more than one drone, `borg_ack entry_id=<id> kind=claim` BEFORE starting \u2014 it announces you are taking it so peers skip the duplicate work, and wakes the rest of the entry's audience. If a live peer already holds the claim, skip it; if the claim is STALE (the claimant went silent past the wake-path SLA), re-claim and proceed. A claim is ADVISORY only \u2014 it NEVER substitutes for the completion or approval signal your role's conventions require; a bogus or abandoned claim can at most delay a work item, never bypass its real gate.\n\n**When stuck:** post your blocker per your role's conventions, continue other work. Escalation is per your role detail, not by stalling.\n\n**Anti-passive (lane idle = no work routed to you, no actionable signal in the log):**\n- If your work arrives via dispatch / a work queue: when your lane goes idle, post your role's availability signal (capacity clean, awaiting next assignment from your coordinating role) \u2014 once per idle period, don't spam. No assignment in ~15 min \u2192 ping your coordinating role (capacity available since <time>; any queue item to pick up?).\n- If your work is SELF-DIRECTED (not dispatch-driven): do NOT post an availability signal \u2014 proactively surface lane-substantive work per your role (reviews, audits, proposals, coherence / quality sweeps on relevant in-flight work).\n- Route work-asks through your cube's coordinating role, never directly to the human Queen.\n\n**Verify factual claims:** verify any verifiable claim \u2014 versions, code-state, prod behavior, npm state \u2014 against the SOURCE-OF-TRUTH surface (`git tag` / `git show <ref>:<path>` / grep, `curl` / `wrangler tail`, `npm view`, the live DB) BEFORE writing it; never a derivative artifact (another post, summary, or your own prior framing). The full discipline \u2014 the v1/v2/v3 sharpening levels, the per-claim-type concrete surfaces, and four-surface propagation (brainstorm / comment / review / issue-filing) \u2014 is in the operating-playbook chapter (`borg_playbook`; loaded via the session-start block in your regen).\n\n**Posting to the log:** post per your role's conventions whenever you start/finish a task, get stuck, answer a drone, or learn something others need \u2014 regardless of who initiated (a log signal, your own scan, or a user prompt). Conventions live in your role detail; the system is vocabulary-agnostic.\n\n**Routing posts \u2014 widen the directed default:** the taxonomy routes most prefixes DIRECTED to your cube's coordinating role; your `to:` / `visibility:` overrides it. Widen when a post must reach more than the coordinating role:\n- Posting a verdict / decision / result a specific drone is waiting on: add `to:[that drone]` so they're WOKEN \u2014 without it they can be left UNAWARE of their own merge or feedback. Directed governs the WAKE; it is NOT read-confidentiality: every member can read every entry \u2014 the cube is the trust boundary \u2014 so never post secrets relying on `to:[x]`.\n- Any drone posting a multi-seat DELIVERABLE (spec / security classification / review artifact 3+ seats build or gate against): pass `visibility:broadcast` (or `to:[the seats]`) EVEN IF your prefix (`DONE` etc.) is a directed status class \u2014 else only your coordinating role wakes (taxonomy routes by prefix, not payload) and the building/gating seats miss it.\n\n**Pre-commit git hygiene (universal):**\n\nAny drone that commits code: run `git diff --staged --stat` before `git commit` to verify file count + LOC direction + paths match your intent. Catches deleted files / anomalous -LOC / wrong paths pre-push. Your role may layer more git rules (code-implementing + coordinating roles typically carry the full set)."}const H=x();function V(){return'## Operating playbook \u2014 full disciplines (borg_playbook chapter)\n\nThis is the on-demand detail behind the rule-spine in your regen. Load it ONCE per session; it is static \u2014 do not re-fetch on every wake.\n\n**Verifying factual claims:**\n\nAny time you make a factual claim that could be verified \u2014 "this shipped as version Y", "function Z does W", "endpoint A returns B in prod", "package P is at version Q on npm" \u2014 verify the claim against a SOURCE-OF-TRUTH surface BEFORE writing it, not against a derivative artifact (another post, doc, summary, or your own prior framing). Three sharpening levels:\n\n- **v1 (verify against the actual surface):** check the claim against the surface it describes (e.g. a code-state claim \u2192 grep the file). Apply when the claim is about code-state.\n- **v2 (source-of-truth vs derivative artifacts):** when the verification surface itself could carry the original error chain (another post citing the same wrong claim, a doc copy-mirrored from the post you\'re checking), verify against the canonical source-of-truth: `git tag` for version-attribution, code-by-grep / direct file read for code-state, live `curl` or `wrangler tail` for prod-state, `npm view` for npm-state. Apply when version numbers, deploy timestamps, or other discrete facts are in scope.\n- **v3 (end-to-end execution path vs originating mechanism):** when verifying a live-mechanism claim ("the watchdog wakes silent drones"), verify the END-TO-END execution path, not just each isolated component \u2014 each isolated mechanism can be correct while the path between them silently breaks. Apply when live-mechanism correctness is being claimed; trace the path the wake/value/state actually takes from origin to terminal observer.\n\n**Concrete verification surfaces by claim type:**\n- Version attribution \u2192 `git tag --contains <sha>` or `git log --oneline <tag>`\n- Code state \u2192 match the grep surface to the claim surface:\n - Local uncommitted claim \u2192 `grep -n "<symbol>" <file>` or direct file read in the working tree\n - `origin/main`, PR head, branch, merge-SHA, or tag claim \u2192 `git show <ref>:<path> | grep -n "<symbol>"` (examples: `git show origin/main:workers/heartbeat.ts | grep -n "last_log_post"`; `git show origin/feat/foo:client/src/log-stream.ts | grep -n "ownDrone"`; `git show abc1234:workers/cubes.ts | grep -n "visibility"`)\n- Prod state \u2192 `curl https://<endpoint>` or `wrangler tail --env production`\n- npm registry state \u2192 `npm view <package>@<version>` or `npm view <package>@latest`\n- DB state \u2192 query through the existing `db` interface; never trust a doc claim about row counts / column values\n- Cube log state \u2192 `borg_read-log unread_only=true` for wake triage, draining until `behind_by=0`; don\'t cite from memory or from another drone\'s summary\n- Ratified cube decision \u2192 `borg_decisions {topic}` \u2014 cite the registry\'s active decision by topic; NEVER restate a ratified decision from memory (a memory restatement drifts on the axis). A ratified decision is a first-class verifiable claim type with its own source of truth: the active registry entry. Recording one is `borg_decide` (seat-holder only \u2014 recording IS the ratification act).\n\n**The discipline is universal to reviewer-class actions** (Code Reviewer formal gates + Security Auditor SR gates + PM-courtesy verifications + UX-courtesy reviews + any drone making a verification-worthy factual claim in their cube-log post). It lives in this universal playbook rather than any one role\'s text because it applies to ALL reviewers.\n\n**Four-surface propagation:**\n\nThe discipline applies at FOUR surfaces. Catches at the surface closest to origin are cheapest; catches at later surfaces have already propagated through earlier consumers:\n\n- **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep\'s the referenced file BEFORE composing the proposal. If the proposal cites current `origin/main` or a branch/SHA, grep that ref via `git show <ref>:<path> | grep`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.\n- **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules\' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep\'s the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via `git show <ref>:<path> | grep`; don\'t let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.\n- **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.\n- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking issue from a cube event payload, the FILING drone fetches the originating entry\'s full body from the cube log BEFORE composing the issue body. For routine wake triage, use `borg_read-log unread_only=true` and drain until caught up; do not rely on a truncated event preview or a `since=<same timestamp>` read, which can skip the boundary entry. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface \u2014 the filed issue becomes the cube\'s durable cross-cycle memory; correcting it requires a follow-up correction post, and later pickup drones inherit the incomplete framing if the correction is missed.\n\n**Ratified-decision drift is a four-surface drift-class.** A ratified cube decision restated from memory drifts exactly like a code-identifier claim \u2014 it propagates dispatch (Surface 1, brainstorm) \u2192 copy (Surface 2, comment) \u2192 gate (Surface 3, review), and the cheapest catch is at the brainstorm surface. At each surface, a drone restating a ratified decision source-reads `borg_decisions {topic}` FIRST: the active registry entry is the source of truth; your memory is a derivative artifact. Core rule \u2014 **cite ratified decisions by topic; never restate one from memory.**'}function D(e){const o=typeof e=="string"?new Date(e):e,r=Date.now()-o.getTime();if(!Number.isFinite(r)||r<0)return"just now";const i=Math.floor(r/1e3);if(i<60)return`${i}s ago`;const t=Math.floor(i/60);if(t<60)return`${t}m ago`;const n=Math.floor(t/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}function I(e){return e==null||Array.isArray(e)&&e.length===0?"Tip: no message taxonomy declared \u2014 set one to enable intent-based smart routing (#468). Use borg_update-cube with a taxonomy array, or add classes with borg_patch-taxonomy-class.":""}function J(e,o){return e.drone?.label??o??null}let f=!1,y=null;function G(){f=!1,y=null}function L(e){const o=e??"",r=k.filter(i=>o.includes(i));return[..._,...r]}function $(e,o){return`rationale \u2192 borg_role-rationale ${JSON.stringify(e)} ${JSON.stringify(o)}`}function K(e){const o=e.match(/borg_role-rationale\s+("(?:(?:\\.)|[^"\\])*")\s+("(?:(?:\\.)|[^"\\])*")/);if(!o)return null;try{return{role:JSON.parse(o[1]),section:JSON.parse(o[2])}}catch{return null}}const P=[..._,...k];function F(e,o){return A(o??"").map(t=>{if(t.kind!=="label"||t.heading==null||!t.heading.trim().toLowerCase().endsWith("rationale")||P.some(p=>t.body.includes(p)))return t.body;const s=t.body.indexOf(`
|
|
4
|
-
`);return(s===-1?
|
|
5
|
-
`:
|
|
6
|
-
`}).join("")}function
|
|
7
|
-
`),
|
|
8
|
-
`)||"_(no drones connected)_",
|
|
9
|
-
`):"",
|
|
10
|
-
`)})(),
|
|
11
|
-
`),"","## Roles in this cube",
|
|
12
|
-
`)}function
|
|
1
|
+
import{ROLE_SCOPED_SAFETY_DISCIPLINES as _,UNIVERSAL_SAFETY_DISCIPLINES as x}from"./templates.js";import{parseRoleSections as C}from"./role-section.js";import{formatRoleAgentLabel as T}from"./roster-render.js";import{formatDroneAddressToken as N}from"./drone-address.js";import{shellEscape as f}from"./shell-escape.js";function q(e){if(!e||!e.trim())return null;try{const t=JSON.parse(e)?.source;return typeof t=="string"?t:null}catch{return null}}function D(e,o,t){return e==="codex"?"Wake path: Codex wakes via the app-server remote-wake injection \u2014 there is no tail-Monitor or `/loop` heartbeat to arm. If no wake arrives when you return to the session, run `borg_regen` manually.":e==="opencode"?"Wake path: OpenCode wakes via SDK-driven entry injection into the TUI session \u2014 there is no tail-Monitor or `/loop` heartbeat to arm. Cube posts are delivered directly into your active session. Use `borg_regen` at any time for fresh context.":["Arm your wake path before working:",`1. **Inbox Monitor** (wake path) \u2014 run a persistent Monitor on \`${t?`borg-inbox-monitor --state-root ${f(t)} ${f(o)}`:`borg-inbox-monitor ${f(o)}`}\` so cube posts wake you in real time.`,"2. **Engage `/loop`** (self-paced) so you keep waking to triage the cube.","3. **Fallback heartbeat** \u2014 set a ~3600s (60-min) `ScheduleWakeup` so you never go fully silent."].join(`
|
|
2
|
+
`)}function H(e,o){return{cubeName:o?.cube?.name??e.name,droneLabel:o?.drone?.label??e.droneLabel,roleName:o?.role?.name??e.roleName??null}}function V(e){const{cubeName:o,droneLabel:t,roleName:n,inboxPath:r,monitorStateRoot:i,agentKind:s,source:c}=e,l=c==="clear"?s==="codex"?"\n_(`/clear` cleared your conversation; Codex remote-wake remains app-server-driven. If no wake arrives, run `borg_regen` manually when returning.)_\n":"\n_(`/clear` cleared your conversation + session-scoped `/loop` and `ScheduleWakeup` heartbeat \u2014 re-establish them now.)_\n":"";return[`# Cube: ${o} \u2014 ${t}`,"",`**Your role:** ${n||"_(call `borg_regen` to load)_"}`,l,"You are a Borg drone \u2014 coordinate through the cube log, and never pause for the user. Blocked \u2192 escalate to your cube's coordinating role.","",D(s,r,i),"","This orientation is intentionally lean. Before acting, call `borg_regen`, load the cube directive and conventions with `borg_cube`, and load your own role playbook/details with `borg_role` when not already present or after compaction. Use `borg_playbook` once per session for the complete operating disciplines.",""].join(`
|
|
3
|
+
`)}function S(){return"## How to operate as a Drone\n\nYou're a Drone in a Cube. Coordinate with other drones through the activity log.\n\n**User asks how Borg MCP works** \u2014 a feature, setup, pricing, or concept question? Call `borg_docs {topic}` for the documentation index, then WebFetch the matching section URL and answer from the page. Don't guess borgmcp's own behavior from memory.\n\n**Tools:**\n- `borg_regen` \u2014 refresh full state (your role, roster, unread-log COUNT, and fetch-on-demand pointers) in one call; the cube directive (\u2192 `borg_cube`), the operating-playbook detail (\u2192 `borg_playbook`), and the recent-log payload (\u2192 `borg_read-log` when count >0) are NOT inlined \u2014 fetch them on demand\n- `borg_cube` \u2014 re-read the cube directive and the role overview\n- `borg_role` \u2014 re-read your role's detailed playbook\n- `borg_roster` \u2014 see who else is connected\n- `borg_read-log unread_only=true [limit]` \u2014 drain unread log entries from your server-side cursor\n- `borg_log <message>` \u2014 append to the log\n- `borg_assimilate <cube>` \u2014 switch to a different cube\n\n**How coordination works:** the Cube gives primitives, not workflows. Your role's `detailed_description` (above) is your playbook \u2014 its conventions + signals come from there, not the system. The log is the coordination channel. Different cubes, different conventions.\n\n**Default: act autonomously, coordinate through the log.** Don't wait for user input. Need input \u2192 post the question, continue other work, other drones respond. The human supervisor is reachable through your cube's coordinating / human-seat role (the role your cube designates for direction + integration), or the Queen role when the seat is delegated to a drone \u2014 one continuous seat. Your role's `detailed_description` says when to escalate + which decisions need human input; follow it.\n\n**Operating loop \u2014 each wake, in order:**\n1. Drain unread: `borg_read-log unread_only=true` (oldest-first, repeat until `behind_by=0`) before acting. The \"Cube log\" section gives your UNREAD COUNT.\n2. Apply your role's conventions to each entry. Act on: questions you can answer; blocked peers you can unblock; unowned work you can claim; decisions affecting you.\n3. Actionable signal \u2192 act + post the convention. Don't wait to be asked.\n4. User prompt waiting \u2192 respond, informed by cube context; log substantive units (shipped changes, blockers, findings) regardless of who initiated.\n5. Nothing actionable + no prompt \u2192 done; wait for next wake.\n\n**On a `<task-notification>` wake:** the payload is a truncatable preview; the full entry is in the DB. Drain: `borg_read-log unread_only=true limit=20`, repeat until `behind_by=0`. Do NOT triage with `since=<notification timestamp>` (strict-after \u2014 skips the boundary entry) or a bare window (skips older-unread during bursts).\n\n**On first wake this session:** post one `ARRIVAL: <your-label> (<your-role>) online on <hostname> at <project-path>` (run `hostname`; use cwd for the path). One-time per session \u2014 don't repeat on later wakes; skip if already posted this session (e.g. after a `/mcp` reconnect).\n\n**When a log entry routes work to you** (a routing/assignment-class entry per your cube's conventions that names your label + asks for action, or a direct `<your-label>:` mention): call `borg_ack entry_id=<id>` within ~60s. Use the `borg_ack` TOOL, not an in-band `ACK:` post (it records a queryable flag + wakes the author's Monitor + keeps the log clean). Ack = receipt, not completion (`STARTING` / `DONE` still apply). Ack only routing-class signals \u2014 not every mention.\n\n**Claim a work item before you start it (`borg_ack ... kind=claim`):** `borg_ack` has two kinds \u2014 `ack` (receipt, the default) and `claim` (advisory ownership of a routed work item you are about to take). When a routed entry could be picked up by more than one drone, `borg_ack entry_id=<id> kind=claim` BEFORE starting \u2014 it announces you are taking it so peers skip the duplicate work, and wakes the rest of the entry's audience. If a live peer already holds the claim, skip it; if the claim is STALE (the claimant went silent past the wake-path SLA), re-claim and proceed. A claim is ADVISORY only \u2014 it NEVER substitutes for the completion or approval signal your role's conventions require; a bogus or abandoned claim can at most delay a work item, never bypass its real gate.\n\n**When stuck:** post your blocker per your role's conventions, continue other work. Escalation is per your role detail, not by stalling.\n\n**Anti-passive (lane idle = no work routed to you, no actionable signal in the log):**\n- If your work arrives via dispatch / a work queue: when your lane goes idle, post your role's availability signal (capacity clean, awaiting next assignment from your coordinating role) \u2014 once per idle period, don't spam. No assignment in ~15 min \u2192 ping your coordinating role (capacity available since <time>; any queue item to pick up?).\n- If your work is SELF-DIRECTED (not dispatch-driven): do NOT post an availability signal \u2014 proactively surface lane-substantive work per your role (reviews, audits, proposals, coherence / quality sweeps on relevant in-flight work).\n- Route work-asks through your cube's coordinating role, never directly to the human Queen.\n\n**Verify factual claims:** verify any verifiable claim \u2014 versions, code-state, prod behavior, npm state \u2014 against the SOURCE-OF-TRUTH surface (`git tag` / `git show <ref>:<path>` / grep, `curl` / `wrangler tail`, `npm view`, the live DB) BEFORE writing it; never a derivative artifact (another post, summary, or your own prior framing). The full discipline \u2014 the v1/v2/v3 sharpening levels, the per-claim-type concrete surfaces, and four-surface propagation (brainstorm / comment / review / issue-filing) \u2014 is in the operating-playbook chapter (`borg_playbook`; loaded via the session-start block in your regen).\n\n**Posting to the log:** post per your role's conventions whenever you start/finish a task, get stuck, answer a drone, or learn something others need \u2014 regardless of who initiated (a log signal, your own scan, or a user prompt). Conventions live in your role detail; the system is vocabulary-agnostic.\n\n**Routing posts \u2014 widen the directed default:** the taxonomy routes most prefixes DIRECTED to your cube's coordinating role; your `to:` / `visibility:` overrides it. Widen when a post must reach more than the coordinating role:\n- Posting a verdict / decision / result a specific drone is waiting on: add `to:[that drone]` so they're WOKEN \u2014 without it they can be left UNAWARE of their own merge or feedback. Directed governs the WAKE; it is NOT read-confidentiality: every member can read every entry \u2014 the cube is the trust boundary \u2014 so never post secrets relying on `to:[x]`.\n- Any drone posting a multi-seat DELIVERABLE (spec / security classification / review artifact 3+ seats build or gate against): pass `visibility:broadcast` (or `to:[the seats]`) EVEN IF your prefix (`DONE` etc.) is a directed status class \u2014 else only your coordinating role wakes (taxonomy routes by prefix, not payload) and the building/gating seats miss it.\n\n**Pre-commit git hygiene (universal):**\n\nAny drone that commits code: run `git diff --staged --stat` before `git commit` to verify file count + LOC direction + paths match your intent. Catches deleted files / anomalous -LOC / wrong paths pre-push. Your role may layer more git rules (code-implementing + coordinating roles typically carry the full set)."}const J=S();function G(){return'## Operating playbook \u2014 full disciplines (borg_playbook chapter)\n\nThis is the on-demand detail behind the rule-spine in your regen. Load it ONCE per session; it is static \u2014 do not re-fetch on every wake.\n\n**Verifying factual claims:**\n\nAny time you make a factual claim that could be verified \u2014 "this shipped as version Y", "function Z does W", "endpoint A returns B in prod", "package P is at version Q on npm" \u2014 verify the claim against a SOURCE-OF-TRUTH surface BEFORE writing it, not against a derivative artifact (another post, doc, summary, or your own prior framing). Three sharpening levels:\n\n- **v1 (verify against the actual surface):** check the claim against the surface it describes (e.g. a code-state claim \u2192 grep the file). Apply when the claim is about code-state.\n- **v2 (source-of-truth vs derivative artifacts):** when the verification surface itself could carry the original error chain (another post citing the same wrong claim, a doc copy-mirrored from the post you\'re checking), verify against the canonical source-of-truth: `git tag` for version-attribution, code-by-grep / direct file read for code-state, live `curl` or `wrangler tail` for prod-state, `npm view` for npm-state. Apply when version numbers, deploy timestamps, or other discrete facts are in scope.\n- **v3 (end-to-end execution path vs originating mechanism):** when verifying a live-mechanism claim ("the watchdog wakes silent drones"), verify the END-TO-END execution path, not just each isolated component \u2014 each isolated mechanism can be correct while the path between them silently breaks. Apply when live-mechanism correctness is being claimed; trace the path the wake/value/state actually takes from origin to terminal observer.\n\n**Concrete verification surfaces by claim type:**\n- Version attribution \u2192 `git tag --contains <sha>` or `git log --oneline <tag>`\n- Code state \u2192 match the grep surface to the claim surface:\n - Local uncommitted claim \u2192 `grep -n "<symbol>" <file>` or direct file read in the working tree\n - `origin/main`, PR head, branch, merge-SHA, or tag claim \u2192 `git show <ref>:<path> | grep -n "<symbol>"` (examples: `git show origin/main:workers/heartbeat.ts | grep -n "last_log_post"`; `git show origin/feat/foo:client/src/log-stream.ts | grep -n "ownDrone"`; `git show abc1234:workers/cubes.ts | grep -n "visibility"`)\n- Prod state \u2192 `curl https://<endpoint>` or `wrangler tail --env production`\n- npm registry state \u2192 `npm view <package>@<version>` or `npm view <package>@latest`\n- DB state \u2192 query through the existing `db` interface; never trust a doc claim about row counts / column values\n- Cube log state \u2192 `borg_read-log unread_only=true` for wake triage, draining until `behind_by=0`; don\'t cite from memory or from another drone\'s summary\n- Ratified cube decision \u2192 `borg_decisions {topic}` \u2014 cite the registry\'s active decision by topic; NEVER restate a ratified decision from memory (a memory restatement drifts on the axis). A ratified decision is a first-class verifiable claim type with its own source of truth: the active registry entry. Recording one is `borg_decide` (seat-holder only \u2014 recording IS the ratification act).\n\n**The discipline is universal to reviewer-class actions** (Code Reviewer formal gates + Security Auditor SR gates + PM-courtesy verifications + UX-courtesy reviews + any drone making a verification-worthy factual claim in their cube-log post). It lives in this universal playbook rather than any one role\'s text because it applies to ALL reviewers.\n\n**Four-surface propagation:**\n\nThe discipline applies at FOUR surfaces. Catches at the surface closest to origin are cheapest; catches at later surfaces have already propagated through earlier consumers:\n\n- **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep\'s the referenced file BEFORE composing the proposal. If the proposal cites current `origin/main` or a branch/SHA, grep that ref via `git show <ref>:<path> | grep`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.\n- **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules\' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep\'s the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via `git show <ref>:<path> | grep`; don\'t let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.\n- **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.\n- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking issue from a cube event payload, the FILING drone fetches the originating entry\'s full body from the cube log BEFORE composing the issue body. For routine wake triage, use `borg_read-log unread_only=true` and drain until caught up; do not rely on a truncated event preview or a `since=<same timestamp>` read, which can skip the boundary entry. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface \u2014 the filed issue becomes the cube\'s durable cross-cycle memory; correcting it requires a follow-up correction post, and later pickup drones inherit the incomplete framing if the correction is missed.\n\n**Ratified-decision drift is a four-surface drift-class.** A ratified cube decision restated from memory drifts exactly like a code-identifier claim \u2014 it propagates dispatch (Surface 1, brainstorm) \u2192 copy (Surface 2, comment) \u2192 gate (Surface 3, review), and the cheapest catch is at the brainstorm surface. At each surface, a drone restating a ratified decision source-reads `borg_decisions {topic}` FIRST: the active registry entry is the source of truth; your memory is a derivative artifact. Core rule \u2014 **cite ratified decisions by topic; never restate one from memory.**'}function I(e){const o=typeof e=="string"?new Date(e):e,t=Date.now()-o.getTime();if(!Number.isFinite(t)||t<0)return"just now";const n=Math.floor(t/1e3);if(n<60)return`${n}s ago`;const r=Math.floor(n/60);if(r<60)return`${r}m ago`;const i=Math.floor(r/60);return i<24?`${i}h ago`:`${Math.floor(i/24)}d ago`}function L(e){return e==null||Array.isArray(e)&&e.length===0?"Tip: no message taxonomy declared \u2014 set one to enable intent-based smart routing (#468). Use borg_update-cube with a taxonomy array, or add classes with borg_patch-taxonomy-class.":""}function K(e,o){return e.drone?.label??o??null}let y=!1,b=null;function Q(){y=!1,b=null}function $(e){const o=e??"",t=_.filter(n=>o.includes(n));return[...x,...t]}function P(e,o){return`rationale \u2192 borg_role-rationale ${JSON.stringify(e)} ${JSON.stringify(o)}`}function X(e){const o=e.match(/borg_role-rationale\s+("(?:(?:\\.)|[^"\\])*")\s+("(?:(?:\\.)|[^"\\])*")/);if(!o)return null;try{return{role:JSON.parse(o[1]),section:JSON.parse(o[2])}}catch{return null}}const F=[...x,..._];function U(e,o){return C(o??"").map(r=>{if(r.kind!=="label"||r.heading==null||!r.heading.trim().toLowerCase().endsWith("rationale")||F.some(l=>r.body.includes(l)))return r.body;const s=r.body.indexOf(`
|
|
4
|
+
`);return(s===-1?r.body+`
|
|
5
|
+
`:r.body.slice(0,s+1))+P(e,r.heading)+`
|
|
6
|
+
`}).join("")}function Z(e,o={}){const t=o.mode??"full",n=e.roles.map(a=>`- **${a.name}**${a.is_default?" _(default)_":""} \u2014 ${a.short_description||"_(no short description)_"}`).join(`
|
|
7
|
+
`),r=e.drones.map(a=>{const h=e.roles.find(p=>p.id===a.role_id),g=T(h?.name??"?",a.agent_kind);return`- **${a.label}** (${g}) \u2014 last seen ${I(new Date(a.last_seen))}`}).join(`
|
|
8
|
+
`)||"_(no drones connected)_",i=typeof e.behind_by=="number"?e.behind_by:null,s=i===null?"Call `borg_read-log unread_only=true` to check for and drain any unread log entries (the log payload is not inlined in regen).":i>0?`You have **${i}** unread log ${i===1?"entry":"entries"}. Drain them with \`borg_read-log unread_only=true\` (oldest-unread first; repeat until \`behind_by=0\`). The log payload is not inlined here \u2014 fetch on demand.`:"You're caught up \u2014 **0** unread log entries. No need to read the log right now.",l=(i??0)===0&&e.drones.length<=1?["## Getting started","","Welcome to your first cube. Here's how to get going:","",'1. Post your first activity: `borg_log message="Starting work on <your task>"`',"2. Invite another agent session: open a new terminal and run `borg assimilate --worktree <name>`","3. Check who's here: `borg_roster`","","---",""].join(`
|
|
9
|
+
`):"",v=L(e.cube.message_taxonomy),E=12,m=Array.isArray(e.decisions)?e.decisions:[],w=(()=>{if(m.length===0)return"";const a=m.slice(0,E),h=a.map(p=>`- **${p.topic}:** ${p.decision}`),g=m.length-a.length;return g>0&&h.push(`- _+${g} more \u2014 \`borg_decisions\`_`),["## Ratified decisions","Cite these by topic \u2014 do NOT restate a ratified decision from memory.",...h].join(`
|
|
10
|
+
`)})(),d=e.role.detailed_description_hash??null,O=e.role.detailed_description?U(e.role.name,e.role.detailed_description):"_(no detailed description set)_",R="Before you post or act, load your full operating context \u2014 once per session; static, do NOT re-fetch on every wake:\n- `borg_playbook` \u2014 your full operating disciplines (verification, four-surface propagation, ack / routing / idle detail).\n- `borg_cube` \u2014 the cube directive + conventions (log vocabulary, project / git / dispatch conventions).",k=t==="full"||d==null||d!==b,A=t==="full"||!y,u=[l+`# Cube: ${e.cube.name} \u2014 ${e.drone.label}`,"",`**Your role:** ${e.role.name}`,""];return t==="lite"&&u.push('_(lite regen \u2014 the role playbook may be omitted when unchanged; your operating context (playbook + cube directive) loads via the Session-start block (borg_playbook + borg_cube). If the playbook is NOT in your current context (e.g. after a context-compaction), call `borg_regen mode="full"` to re-orient.)_',""),u.push(t==="full"?"## Session start \u2014 required before acting":"## Session start",t==="full"?R:'Operating context (playbook + cube directive) was loaded at session start \u2014 re-fetch `borg_playbook` / `borg_cube` ONLY after a context-compaction (a `mode="full"` regen), not on every wake.',"",...v?[v,""]:[],`## Your role: ${e.role.name}`,k?O:["_(role playbook unchanged since your last full/lite regen; omitted in lite mode)_","",...$(e.role.detailed_description)].join(`
|
|
11
|
+
`),"","## Roles in this cube",n,"","## Connected drones",r,"","## Cube log",s,...w?["",w]:[]),A&&(u.push("",S()),y=!0),k&&d!=null&&(b=d),u.join(`
|
|
12
|
+
`)}function z(e,o,t){const n=o.get(e.drone_id),r=n?t.get(n.role_id):null,i=new Date(e.created_at).toISOString(),s=typeof e.id=="string"&&e.id.length>0?` [entry_id: ${e.id}]`:"",c=typeof e.drone_id=="string"&&e.drone_id.length>0?` ${N(e.drone_id)}`:"";return`**[${i}]**${s}${c} ${n?.label??"?"} (${r?.name??"?"}): ${e.message}`}export{J as DRONE_PLAYBOOK,Q as __resetRegenSessionState,U as compressRoleText,V as formatLeanOrientation,z as formatLogEntryMarkdown,P as formatRationalePointer,Z as formatRegenMarkdown,S as getDronePlaybook,G as getDronePlaybookChapter,I as humanAgo,L as nullTaxonomyTip,q as parseHookSource,X as parseRationalePointer,K as regenWakePathDroneLabel,H as resolveLeanIdentity,D as wakePathArming};
|
package/dist/regen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{regen as
|
|
3
|
-
`)}async function
|
|
2
|
+
import{regen as a,listCubes as c}from"./remote-client.js";import{findProjectRoot as u,getActiveCube as h,inboxPathForDrone as l}from"./cubes.js";import{monitorStateRootForWorktree as p}from"./inbox-monitor.js";import{parseHookSource as b,formatLeanOrientation as m,resolveLeanIdentity as g}from"./regen-format.js";import{resolveSessionAgentKind as d}from"./codex-app-wake.js";import{handleVersionFlag as f}from"./version.js";import{gateAllowsActivation as w}from"./launch-gate.js";async function y(){if(process.stdin.isTTY)return"";try{const e=[];for await(const t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf-8")}catch{return""}}async function v(){if(f(),!w("borg-regen SessionStart hook"))return;const e=b(await y()),t=await h();if(!t){await _();return}const o=l(t.cubeId,t.droneId),n=p(u()),r=d();let s=null;try{s=await a(t.sessionToken,t.apiUrl)}catch{s=null}const i=g(t,s);process.stdout.write(m({...i,inboxPath:o,monitorStateRoot:n,agentKind:r,source:e})+`
|
|
3
|
+
`)}async function _(){const e=[];e.push("# Borg MCP \u2014 not connected to a cube"),e.push(""),e.push("The borg MCP server is installed in this project but no cube has been joined here yet."),e.push(""),e.push("**Drone instructions:**"),e.push("1. **Tell the user** which cubes exist on their account (see the list below) and ask them which one to join, or whether to create a new cube. This is the first thing you should surface in this session."),e.push("2. **Do not** call `borg_assimilate`, `borg_create-cube`, or any other state-changing borg tool until the user has chosen."),e.push("3. The user may also decline to use borg in this project at all \u2014 that's a valid choice; just stop suggesting it."),e.push("");try{const{cubes:t}=await c();if(t.length>0){e.push("## Cubes on your account");for(const o of t)e.push(`- **${o.name}** (id: ${o.id})`)}else e.push("## Cubes on your account"),e.push("_(none yet \u2014 offer to create one via `borg_create-cube` once the user confirms)_")}catch(t){e.push("## Cubes on your account"),e.push("_(could not list \u2014 "+(t?.message??String(t))+")_")}e.push(""),e.push("## Tools you can call once the user has chosen"),e.push('- Join an existing cube: `borg_assimilate cube_name="<their choice>"`'),e.push('- Create a new cube: `borg_create-cube name="<name>" cube_directive="<markdown>"` (optionally `template="software-dev"`)'),e.push("- See available templates: `borg_list-templates`"),process.stdout.write(e.join(`
|
|
4
4
|
`)+`
|
|
5
|
-
`)}
|
|
5
|
+
`)}v().catch(e=>{const t=e?.message??String(e);process.stderr.write(`borg-regen: ${t}
|
|
6
6
|
`),process.exit(1)});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type McpStartupTask = () => void | Promise<void>;
|
|
2
|
+
/** Required named services: omission from index wiring is a type error. */
|
|
3
|
+
export interface McpStartupServices {
|
|
4
|
+
sessionStartHook: McpStartupTask;
|
|
5
|
+
auditHook: McpStartupTask;
|
|
6
|
+
sseStream: McpStartupTask;
|
|
7
|
+
openCode: McpStartupTask;
|
|
8
|
+
healthBeat: McpStartupTask;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run normal long-lived MCP child startup work. The assimilation readiness
|
|
12
|
+
* child must reach the initialize response without acquiring leases, fetching
|
|
13
|
+
* SSE, mutating hooks, or starting timers because it is intentionally killed
|
|
14
|
+
* immediately after that response.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runMcpStartupServices(readinessProbe: boolean, services: McpStartupServices): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=startup-services.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function r(o,t){if(o)return;const a=[t.sessionStartHook,t.auditHook,t.sseStream,t.openCode,t.healthBeat];for(const n of a)try{await n()}catch{}}export{r as runMcpStartupServices};
|
package/dist/stream-owner.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const STREAM_OWNER_STALE_MS = 70000;
|
|
2
|
+
/** Grace window for mkdir→owner.json initialization before an empty lock is reclaimable. */
|
|
3
|
+
export declare const STREAM_OWNER_INIT_STALE_MS = 5000;
|
|
2
4
|
export interface StreamOwnerRecord {
|
|
3
5
|
schemaVersion: number;
|
|
4
6
|
pid: number;
|
|
@@ -8,7 +10,7 @@ export interface StreamOwnerRecord {
|
|
|
8
10
|
heartbeatAt: string;
|
|
9
11
|
}
|
|
10
12
|
export interface StreamOwnershipSnapshot {
|
|
11
|
-
state: 'owner' | 'owned-by-other-process' | 'unowned';
|
|
13
|
+
state: 'owner' | 'owned-by-other-process' | 'initializing' | 'orphaned-initialization' | 'unowned';
|
|
12
14
|
pid?: number;
|
|
13
15
|
processNonce?: string;
|
|
14
16
|
cwd?: string;
|
|
@@ -16,6 +18,8 @@ export interface StreamOwnershipSnapshot {
|
|
|
16
18
|
heartbeatAt?: string;
|
|
17
19
|
ageMs?: number;
|
|
18
20
|
lockPath?: string;
|
|
21
|
+
/** Directory mtime used to compare-before-reap an orphaned initialization. */
|
|
22
|
+
lockMtimeMs?: number;
|
|
19
23
|
}
|
|
20
24
|
export interface StreamLease {
|
|
21
25
|
lockPath: string;
|
|
@@ -32,6 +36,8 @@ export interface StreamOwnerDeps {
|
|
|
32
36
|
processStartedAt?: string;
|
|
33
37
|
isPidAlive?: (pid: number) => boolean;
|
|
34
38
|
beforeTakeoverVerify?: (takeoverPath: string) => Promise<void>;
|
|
39
|
+
/** Initialization/refresh writer seam for failure-path regression tests. */
|
|
40
|
+
writeRecord?: (lockPath: string, record: StreamOwnerRecord) => Promise<void>;
|
|
35
41
|
}
|
|
36
42
|
export declare function streamLockPath(cubeId: string, droneId: string, locksDir?: string): string;
|
|
37
43
|
export declare function acquireStreamLease(cubeId: string, droneId: string, staleMs?: number, deps?: StreamOwnerDeps): Promise<StreamLease | null>;
|
package/dist/stream-owner.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID as
|
|
2
|
-
`,{mode:384}),await a.rename(
|
|
1
|
+
import{randomUUID as O}from"node:crypto";import{promises as a}from"node:fs";import{homedir as _}from"node:os";import u from"node:path";const D=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,R=u.join(_(),".config","borgmcp","stream-locks"),m="owner.json",S=1,F=7e4,$=5e3,l=O(),L=new Date().toISOString();function y(t,r,n=R){return A("cubeId",t),A("droneId",r),u.join(n,t,`${r}.lock`)}async function J(t,r,n=F,e={}){const i=y(t,r,e.locksDir);await a.mkdir(u.dirname(i),{recursive:!0,mode:448});const o=await d(i,e);if(o)return o;const c=await V(t,r,e);if(c.state==="unowned")return d(i,e);if(c.state==="initializing")return null;if(c.state==="orphaned-initialization")return await E(i,c,n,e)?d(i,e):null;if(c.state!=="owned-by-other-process")return null;const s=(c.ageMs??0)>n,w=typeof c.pid=="number"&&e.isPidAlive!==void 0&&!e.isPidAlive(c.pid);return!s&&!w||!await E(i,c,n,e)?null:d(i,e)}async function V(t,r,n={}){const e=y(t,r,n.locksDir);let i;try{i=await a.readFile(u.join(e,m),"utf8")}catch(h){if(h?.code==="ENOENT")try{const f=await a.stat(e),M=(n.now??(()=>new Date))(),I=Math.max(0,M.getTime()-f.mtimeMs);return{state:I>=$?"orphaned-initialization":"initializing",ageMs:I,lockPath:e,lockMtimeMs:f.mtimeMs}}catch(f){if(f?.code==="ENOENT")return{state:"unowned",lockPath:e};throw f}throw h}let o;try{o=JSON.parse(i)}catch{return{state:"owned-by-other-process",lockPath:e,ageMs:Number.POSITIVE_INFINITY}}if(!T(o))return{state:"owned-by-other-process",lockPath:e,ageMs:Number.POSITIVE_INFINITY};const c=(n.now??(()=>new Date))(),s=Date.parse(o.heartbeatAt),w=Number.isFinite(s)?c.getTime()-s:Number.POSITIVE_INFINITY,N=n.pid??process.pid,g=n.processNonce??l;return{state:o.pid===N&&o.processNonce===g?"owner":"owned-by-other-process",pid:o.pid,processNonce:o.processNonce,cwd:o.cwd,startedAt:o.startedAt,heartbeatAt:o.heartbeatAt,ageMs:w,lockPath:e}}async function d(t,r){try{await a.mkdir(t,{mode:448})}catch(e){if(e?.code==="EEXIST")return null;throw e}const n=x(r);try{await(r.writeRecord??b)(t,n)}catch(e){throw await j(t,n).catch(()=>{}),e}return k(t,n,r)}async function j(t,r){const n=`${t}.failed-${r.processNonce}-${Date.now()}`;try{await a.rename(t,n)}catch(i){if(i?.code==="ENOENT")return;throw i}const e=await p(n);if(e&&(e.pid!==r.pid||e.processNonce!==r.processNonce)){try{await a.rename(n,t)}catch(i){if(i?.code!=="EEXIST")throw i;await a.rm(n,{recursive:!0,force:!0})}return}await a.rm(n,{recursive:!0,force:!0})}async function E(t,r,n,e){const i=`${t}.takeover-${e.processNonce??l}-${Date.now()}`;try{await a.rename(t,i)}catch(s){if(s?.code==="ENOENT")return!1;throw s}await e.beforeTakeoverVerify?.(i);const o=await p(i),c=await a.stat(i).catch(()=>null);if(!v(r,o,c?.mtimeMs,n,e)){try{await a.rename(i,t)}catch(s){if(s?.code!=="EEXIST")throw s;await a.rm(i,{recursive:!0,force:!0})}return!1}return await a.rm(i,{recursive:!0,force:!0}),!0}function v(t,r,n,e,i){if(!r)return t.state==="orphaned-initialization"?t.lockMtimeMs!==void 0&&n===t.lockMtimeMs:t.ageMs===Number.POSITIVE_INFINITY;if(t.pid!==r.pid||t.processNonce!==r.processNonce||t.heartbeatAt!==r.heartbeatAt)return!1;const o=(i.now??(()=>new Date))(),c=Date.parse(r.heartbeatAt),w=(Number.isFinite(c)?o.getTime()-c:Number.POSITIVE_INFINITY)>e,N=i.isPidAlive!==void 0&&!i.isPidAlive(r.pid);return w||N}function k(t,r,n){return{lockPath:t,record:r,async refresh(){const e=await p(t);if(!e||e.pid!==r.pid||e.processNonce!==r.processNonce)return!1;const i={...r,heartbeatAt:(n.now??(()=>new Date))().toISOString()};return await(n.writeRecord??b)(t,i),this.record=i,!0},async release(){const e=await p(t);e?.pid===r.pid&&e.processNonce===r.processNonce&&await a.rm(t,{recursive:!0,force:!0})}}}async function p(t){try{const r=await a.readFile(u.join(t,m),"utf8"),n=JSON.parse(r);return T(n)?n:null}catch{return null}}async function b(t,r){const n=u.join(t,m),e=u.join(t,`${m}.${r.processNonce}.tmp`);await a.writeFile(e,JSON.stringify(r,null,2)+`
|
|
2
|
+
`,{mode:384}),await a.rename(e,n)}function x(t){const r=t.now??(()=>new Date);return{schemaVersion:S,pid:t.pid??process.pid,processNonce:t.processNonce??l,cwd:t.cwd??process.cwd(),startedAt:t.processStartedAt??L,heartbeatAt:r().toISOString()}}function T(t){return t!==null&&typeof t=="object"&&t.schemaVersion===S&&typeof t.pid=="number"&&Number.isInteger(t.pid)&&typeof t.processNonce=="string"&&typeof t.cwd=="string"&&typeof t.startedAt=="string"&&typeof t.heartbeatAt=="string"}function A(t,r){if(!D.test(r))throw new Error(`Invalid ${t}: ${r}`)}export{$ as STREAM_OWNER_INIT_STALE_MS,F as STREAM_OWNER_STALE_MS,J as acquireStreamLease,V as readOwnershipSnapshot,y as streamLockPath};
|
package/dist/stream-status.d.ts
CHANGED
|
@@ -42,13 +42,14 @@ import type { StreamStatus } from './log-stream.js';
|
|
|
42
42
|
* Linux per package.json `os` field; the null branch handles other
|
|
43
43
|
* platforms gracefully).
|
|
44
44
|
*/
|
|
45
|
-
export declare function checkInboxMonitorHealthy(inboxPath: string | null): boolean | null;
|
|
45
|
+
export declare function checkInboxMonitorHealthy(inboxPath: string | null, monitorStateRoot?: string | null): boolean | null;
|
|
46
46
|
/**
|
|
47
|
-
* gh#822:
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* gh#822: are all present holder heartbeat sidecars stale past the threshold?
|
|
48
|
+
* A fresh legacy sidecar wins during migration; absent sidecars (old monitor /
|
|
49
|
+
* just-armed) still fall back to process presence. Only present-and-all-stale
|
|
50
|
+
* state is a wedged-holder signal.
|
|
50
51
|
*/
|
|
51
|
-
export declare function isHeartbeatStale(inboxPath: string): boolean;
|
|
52
|
+
export declare function isHeartbeatStale(inboxPath: string, monitorStateRoot?: string | null): boolean;
|
|
52
53
|
export interface RenderInputs {
|
|
53
54
|
status: StreamStatus;
|
|
54
55
|
/**
|
|
@@ -62,6 +63,8 @@ export interface RenderInputs {
|
|
|
62
63
|
* mode but omit the exact command.
|
|
63
64
|
*/
|
|
64
65
|
inboxPath: string | null;
|
|
66
|
+
/** Explicit worktree-local root for the Monitor command, when known. */
|
|
67
|
+
monitorStateRoot?: string | null;
|
|
65
68
|
/** Drone label for the Monitor description copy. */
|
|
66
69
|
droneLabel: string | null;
|
|
67
70
|
/** Cube name for the Monitor description copy. */
|
|
@@ -108,6 +111,7 @@ export declare function shouldShowWakePathWarning(streamStatus: StreamStatus, in
|
|
|
108
111
|
*/
|
|
109
112
|
export declare function formatWakePathPrefix(inputs: {
|
|
110
113
|
inboxPath: string | null;
|
|
114
|
+
monitorStateRoot?: string | null;
|
|
111
115
|
droneLabel: string | null;
|
|
112
116
|
cubeName: string | null;
|
|
113
117
|
}): string;
|
package/dist/stream-status.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{spawnSync as
|
|
2
|
-
`)}function
|
|
3
|
-
`)}export{
|
|
1
|
+
import{spawnSync as b}from"node:child_process";import{statSync as $}from"node:fs";import{heartbeatPathFor as _,legacyHeartbeatPathFor as k,HEARTBEAT_STALE_MS as y}from"./inbox-monitor.js";import{shellEscape as u}from"./shell-escape.js";function x(n,t){if(!n)return null;try{const o=b("pgrep",["-f",n],{encoding:"utf-8",timeout:2e3});return o.error?null:o.status===0&&o.stdout.trim().length>0?!A(n,t):o.status===1?!1:null}catch{return null}}function A(n,t){const o=[_(n,t)];t&&o.push(k(n));let s=!1;for(const a of new Set(o))try{const c=$(a).mtimeMs;if(s=!0,Date.now()-c<=y)return!1}catch{}return s}function E(n){const{status:t,inboxMonitorHealthy:o,inboxPath:s,monitorStateRoot:a,droneLabel:c,cubeName:p,humanAgo:l}=n,m=t.reconnectAttempts===0&&t.lastWireActivityAt===null&&!t.connected,h=t.ownership?.state==="owned-by-other-process",d=t.ownership?.state==="orphaned-initialization",w=t.ownership?.state==="initializing";let r;d?r="**Stream blocked by an orphaned initialization lock.**":h?r="**Stream owned by another Borg MCP process.**":m?r="**Stream not started.**":t.connected?o===!1?r="**Stream connected (no inbox-Monitor \u2014 wake path broken).**":t.lastContentEventAt===null?r="**Stream connected, awaiting first content event.**":r=`**Stream connected, last content ${l(new Date(t.lastContentEventAt))}.**`:r=`**Stream disconnected (reconnect attempt ${t.reconnectAttempts}).**`;const e=[];if(e.push(r),e.push(""),e.push("# Log-stream status"),e.push(""),d?e.push("- **state**: _(orphaned stream-owner initialization)_"):w?e.push("- **state**: _(stream-owner initialization in progress)_"):h?e.push("- **state**: _(stream owner is another local process)_"):m?e.push("- **state**: _(stream not started)_"):e.push(`- **connected**: ${t.connected}`),e.push(`- **last content event**: ${t.lastContentEventAt?`${t.lastContentEventAt} (${l(new Date(t.lastContentEventAt))})`:"_(none yet)_"}`),e.push(`- **last heartbeat at**: ${t.lastHeartbeatAt?`${t.lastHeartbeatAt} (${l(new Date(t.lastHeartbeatAt))})`:"_(none)_"}`),e.push(`- **last wire activity**: ${t.lastWireActivityAt?`${t.lastWireActivityAt} (${l(new Date(t.lastWireActivityAt))})`:"_(none)_"}`),e.push(`- **last persisted event id**: ${t.lastPersistedEventId??"_(none)_"}`),e.push(`- **reconnect attempts**: ${t.reconnectAttempts}`),e.push(`- **run-loop restarts**: ${t.runLoopRestartCount}`),e.push(`- **stream ownership**: ${t.ownership?.state??"_(unknown)_"}`),t.ownership?.lockPath&&e.push(`- **ownership lock path**: ${t.ownership.lockPath}`),h){const i=t.ownership;e.push(`- **stream owner pid**: ${i.pid??"_(unknown)_"}`),e.push(`- **stream owner cwd**: ${i.cwd??"_(unknown)_"}`),e.push(`- **stream owner heartbeat age**: ${typeof i.ageMs=="number"&&Number.isFinite(i.ageMs)?`${Math.max(0,Math.round(i.ageMs/1e3))}s`:"_(unknown)_"}`)}return t.connected&&o===!1&&(e.push("- **inbox-monitor**: _(no watcher detected \u2014 wake path broken)_"),s&&c&&p&&(e.push(""),e.push("## Real-time wake-up (do this now)"),e.push("No process is tailing this drone's inbox file \u2014 this session won't wake on real-time cube activity, only on its fallback timer, and will miss live coordination from other drones. Arm an inbox Monitor:"),e.push(""),e.push(`> Monitor command: \`${f(s,a)}\` \u2014 persistent, 1h timeout, description "borg inbox for ${c} on cube ${p}".`))),e.join(`
|
|
2
|
+
`)}function C(n,t){return n.connected&&t===!1}function H(n){const{inboxPath:t,monitorStateRoot:o,droneLabel:s,cubeName:a}=n;return!t||!s||!a?"":["## \u26A0 Wake path broken \u2014 arm Monitor NOW","","No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session only wakes on the /loop fallback heartbeat and will miss live coordination from other drones:","",`> Monitor command: \`${f(t,o)}\` \u2014 persistent, 1h timeout, description "borg inbox for ${s} on cube ${a}".`,"","---",""].join(`
|
|
3
|
+
`)}function f(n,t){return t?`borg-inbox-monitor --state-root ${u(t)} ${u(n)}`:`borg-inbox-monitor ${u(n)}`}export{x as checkInboxMonitorHealthy,H as formatWakePathPrefix,A as isHeartbeatStale,E as renderStreamStatus,C as shouldShowWakePathWarning};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
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",
|