bitfolk 0.1.13 → 0.1.14

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 CHANGED
@@ -315,7 +315,9 @@ turn a limit off again, for example `bitfolk set --max-usd none`.
315
315
  | `--timeout` | off | Minutes before a run is stopped |
316
316
  | `--permission` | auto | `auto`: the runtime's auto mode. `safe`: edits in its worktree, `bitfolk-api`, read-mostly git, no curl. `bypass`: no prompts (only on a machine you'd hand over) |
317
317
 
318
- A session's sign-in to Bitfolk lasts 10 hours when no timeout is set.
318
+ A session's sign-in to Bitfolk lasts for as long as its runner keeps reporting: the runner reports every few minutes
319
+ while the session runs. If the runner stops (the computer sleeps or loses power), the session is marked failed within
320
+ the hour.
319
321
 
320
322
  Bitfolk's own approval rules (`POST /v1/actions`) apply on top, wherever the agent runs.
321
323
 
package/dist/cli.js CHANGED
@@ -53,13 +53,13 @@ printf '%s\\n' "\${out%"$nl"*}"
53
53
  `).filter(Boolean).length}}catch{return{dir:t,changed:0}}}function $i(e){if(!e.repo)return;let t=c(k(),`worktrees`,e.slug);if(b(t))try{Gi(e.repo,[`worktree`,`remove`,`--force`,t])}catch{}}function ea(e){try{return X(e,[`rev-parse`,`HEAD`])}catch{return null}}function ta(e,t,n){let r=`refs/heads/${n}`,i=X(e,[`log`,`--format=%h%x09%s`,`-n`,`50`,`${t}..${r}`]).split(`
54
54
  `).filter(Boolean).map(e=>{let[t=``,...n]=e.split(` `);return{sha:t,subject:n.join(` `).slice(0,200)}}),a=X(e,[`diff`,`--no-ext-diff`,`--no-textconv`,`--shortstat`,t,r]),o=e=>Number(e.exec(a)?.[1]??0);return{commits:i,commit_count:Number(X(e,[`rev-list`,`--count`,`${t}..${r}`]))||i.length,files_changed:o(/(\d+) files? changed/),insertions:o(/(\d+) insertions?/),deletions:o(/(\d+) deletions?/)}}function na(e){let t=/^(?:https:\/\/(?:[^@/]+@)?github\.com\/|ssh:\/\/git@github\.com(?::\d+)?\/|git@github\.com:)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/.exec(e.trim());return t?`${t[1]}/${t[2]}`:null}const ra=/^(?!-)(?!.*\.\.)[A-Za-z0-9._/-]{1,200}$/;function ia(e,t){let n=t?.replace(/^(?:refs\/heads\/|refs\/remotes\/origin\/|origin\/)/,``);if(n&&ra.test(n)&&!/^[0-9a-f]{7,64}$/.test(n)&&n!==`HEAD`)return n;try{let t=Gi(e,[`symbolic-ref`,`--quiet`,`--short`,`refs/remotes/origin/HEAD`]).replace(/^origin\//,``);if(ra.test(t))return t}catch{}return`main`}function aa(e,n){if(!ra.test(n))return null;try{return t(`git`,[`-C`,e,`-c`,`core.hooksPath=/dev/null`,`fetch`,`--quiet`,`--no-tags`,`origin`,`+refs/heads/${n}:refs/remotes/origin/${n}`],{stdio:[`ignore`,`pipe`,`pipe`],env:{...process.env,GIT_TERMINAL_PROMPT:`0`,GCM_INTERACTIVE:`never`},timeout:6e4}),Gi(e,[`rev-parse`,`--verify`,`refs/remotes/origin/${n}^{commit}`])}catch{return null}}function oa(e,n,r){if(!/^bitfolk\/[A-Za-z0-9._-]+$/.test(n))return{error:`not a builder branch`};let i;try{i=Gi(e,[`config`,`--get`,`remote.origin.url`])}catch{return{error:`the repo has no origin remote`}}let a=na(i);if(!a)return{error:`origin isn't a GitHub repository`};let o;try{o=X(e,[`rev-parse`,`--verify`,`refs/heads/${n}`])}catch{return{error:`${n} is gone`}}try{t(`git`,[`-C`,e,`-c`,`core.hooksPath=/dev/null`,`push`,`--quiet`,`--no-verify`,`origin`,`refs/heads/${n}:refs/heads/${n}`],{encoding:`utf8`,stdio:[`ignore`,`pipe`,`pipe`],env:{...process.env,GIT_TERMINAL_PROMPT:`0`,GCM_INTERACTIVE:`never`},timeout:12e4})}catch(e){let t=e;return{error:((t.stderr||t.message||`push failed`).trim().split(`
55
55
  `).filter(Boolean).pop()??`push failed`).slice(0,300)}}return{repo:a,branch:n,base:ia(e,r),head_sha:o}}const sa=e=>`refs/bitfolk/checkpoints/${e}`,ca={GIT_AUTHOR_NAME:`Bitfolk checkpoint`,GIT_AUTHOR_EMAIL:`checkpoint@bitfolk.invalid`,GIT_COMMITTER_NAME:`Bitfolk checkpoint`,GIT_COMMITTER_EMAIL:`checkpoint@bitfolk.invalid`};function la(e,t){let n;try{n=X(e,[`rev-parse`,`HEAD`])}catch{return null}let r=ne(c(fe(),`bitfolk-checkpoint-`));try{let i=c(r,`index`),a=u(e,X(e,[`rev-parse`,`--git-path`,`index`]));b(a)&&ee(a,i);let o={...process.env,...ca,GIT_INDEX_FILE:i};X(e,[`add`,`-A`],o);let s=X(e,[`write-tree`],o),l=sa(t),d=null;try{let[t=``,n=``]=X(e,[`log`,`-1`,`--format=%T %P`,l]).split(` `);d={tree:t,parent:n}}catch{}if(d&&d.tree===s&&d.parent===n)return{head:n,checkpoint:X(e,[`rev-parse`,l]),changed:!1};let f=X(e,[`commit-tree`,s,`-p`,n,`-m`,`Bitfolk checkpoint of session ${t}`],o);return X(e,[`update-ref`,l,f]),{head:n,checkpoint:f,changed:!0}}finally{S(r,{recursive:!0,force:!0})}}function ua(e,t){if(!/^[0-9a-f]{7,64}$/.test(t))return!1;try{let n=X(e,[`rev-parse`,`HEAD`]),[r=``,i=``]=X(e,[`log`,`-1`,`--format=%T %P`,t]).split(` `);return i!==n||r===X(e,[`rev-parse`,`HEAD^{tree}`])?!1:(X(e,[`restore`,`--source=${t}`,`--worktree`,`--`,`.`]),!0)}catch{return!1}}function da(e,t=30,n=Date.now()){let r=0;try{let i=n/1e3-t*86400;for(let t of Gi(e,[`for-each-ref`,`--format=%(refname) %(committerdate:unix)`,`refs/bitfolk/checkpoints/`]).split(`
56
- `)){let[n,a]=t.split(` `);n&&Number(a)<i&&(Gi(e,[`update-ref`,`-d`,n]),r++)}}catch{}return r}const fa=/^[a-z0-9][a-z0-9._-]{0,39}$/;function pa(e){return i(e).toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[^a-z0-9]+/,``).slice(0,40).replace(/[-.]+$/,``)||`repo`}function ma(e){let t=e.repos?.length?e.repos:e.repo?[{name:pa(e.repo),path:e.repo}]:[],n=t.find(t=>t.path===e.repo);return n?[n,...t.filter(e=>e!==n)]:t}const ha=e=>ma(e)[0]??null;function ga(e,t){if(!t)return ha(e);let n=ma(e).find(e=>e.name===t.toLowerCase());if(!n)throw Error(`this computer has no repo named ${t} (it has: ${ma(e).map(e=>e.name).join(`, `)||`none`})`);return n}function _a(e){try{return t(`git`,[`-C`,e,`config`,`--get`,`remote.origin.url`],{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()}catch{return null}}function va(e){return ma(e).map((e,t)=>{let n=_a(e.path);return{name:e.name,default:t===0,github:n?na(n):null}})}function ya(e,t,n,r=!1){let i=(n??ma(e).find(e=>e.path===t)?.name??pa(t)).toLowerCase();if(!fa.test(i))throw Error(`a repo name is 1 to 40 lowercase letters, digits, dots, dashes or underscores`);let a=ma(e).filter(e=>e.path!==t);if(a.some(e=>e.name===i))throw Error(`there's already a repo named ${i}; pick another with --name`);if(a.length>=20)throw Error(`a computer can have at most 20 repos`);return{repos:[...a,{name:i,path:t}],repo:r||!e.repo||!a.length?t:e.repo}}function ba(e,t){let n=ma(e),r=n.find(e=>e.name===t.toLowerCase());if(!r)throw Error(`no repo named ${t} here (it has: ${n.map(e=>e.name).join(`, `)||`none`})`);let i=n.filter(e=>e!==r);return{repos:i,repo:e.repo===r.path?i[0]?.path:e.repo}}const xa=(e,t)=>new Promise(n=>{if(t.aborted)return n();let r=setTimeout(i,e);r.unref?.(),t.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),t.removeEventListener(`abort`,i),n()}});var Sa=class{#e;#t=new AbortController;done;constructor(e){this.#e=e,this.done=this.#n()}stop(){this.#t.abort()}async#n(){let e=this.#t.signal,t=this.#e.sleep??xa,n,r=0;for(;!e.aborted;){let i;try{i=await this.#e.ask(n,n===void 0?0:25,e)}catch(n){if(e.aborted)return;if(n instanceof J&&n.ended||n instanceof O&&(n.status===401||n.status===403)){this.#e.onEnd?.(`signed_out`);return}r++,await t(Math.min(3e5,5e3*2**(r-1)),e);continue}if(i===null){this.#e.onEnd?.(`unsupported`);return}r=0,i.wake&&n!==void 0&&!e.aborted&&this.#e.onWake(),n=i.signature}}};const Ca=36e5,wa=process.env.BITFOLK_FAST_POLL===`1`,Ta=wa?3e3:3e4,Ea=wa?3e4:6e5,Da=wa?2e3:15e3,Oa=wa?5e3:6e4,ka=wa?1e3:5e3,Aa={session_requested:`a session you asked for`,claimable_tasks:`tasks to claim`,my_tasks:`its tasks`,new_messages:`new messages`,mentioned:`a message that tags it`,review_waiting:`a review`,briefing_due:`a briefing`,feedback_ask:`a question from Bitfolk`,setup_actions:`setup`,watch_fired:`a watch`},ja=e=>e.map(e=>Aa[e]??e.replace(/_/g,` `)).join(`, `)||`new work`;function Ma(e,t){if(e){if(e.pid)try{process.kill(-e.pid,t);return}catch{}e.kill(t)}}const Na=new WeakSet;function Pa(e,t=1e4){Ma(e,`SIGTERM`),setTimeout(()=>{e&&!Na.has(e)&&Ma(e,`SIGKILL`)},t).unref()}function Fa(e){let t=Math.round(e/1e3);return t<60?`${t}s`:`${Math.floor(t/60)}m${String(t%60).padStart(2,`0`)}s`}function Ia(e){let t=[];e.costMicros!==null&&t.push(`$${(e.costMicros/1e6).toFixed(e.costMicros>0&&e.costMicros<1e4?4:2)}`);let n=e.inputTokens+e.outputTokens;return n&&t.push(n>=1e3?`${Math.round(n/1e3)}k tokens`:`${n} tokens`),t.join(`, `)}const La={info:[()=>F(L.skip),e=>e],idle:[()=>F(L.idle),F],start:[()=>I.accent(L.dot),e=>e],ok:[()=>I.ok(L.ok),e=>e],fail:[()=>I.err(L.fail),I.err],warn:[()=>I.warn(L.warn),I.warn],progress:[()=>F(L.arrow),F]};function Ra(e,t,n){let r=(t.runStarts??[]).filter(e=>e>n-Ca).sort((e,t)=>e-t),i=e.maxRunsPerHour===void 0?1/0:e.maxRunsPerHour-r.length;return{used:r.length,left:i,freesAt:i>0?n:(r[0]??n)+Ca}}function za(e,t){if(e.command)return{runtime:`command`,command:e.command};if(t.startsWith(`custom:`)){let n=e.customTools?.find(e=>`custom:${e.name}`===t);if(!n)throw Error(`your workspace asked for ${t.slice(7)}, which isn't set up here (${w} add)`);return n.protocol===`acp`?{runtime:`acp`,acp:{...ot(n.command),agent:t}}:{runtime:`command`,command:n.command}}let n=Ze(t);if(n)return{runtime:n};let r=Kr(t);if(r)return{runtime:`acp`,acp:r};throw Error(`this runner can't start ${t}`)}function Ba(e,t){let n=t.session.retry,r=n?.mode===`fix`&&n.branch?.startsWith(`bitfolk/builder-`)?n.branch.slice(8):null,i=ga(e,t.session.repo)?.path,a=e.pullRequests&&i&&!n?aa(i,ia(i,e.base)):null;return{id:t.builder.id,name:t.builder.name,slug:r&&an(r)===r?r:an(`builder-${t.session.id.slice(-8)}`),...za(e,t.session.runtime),api:e.api,token:t.builder.token,repo:i,base:(n?.mode===`redo`&&/^[0-9a-f]{7,64}$/.test(n.base_sha??``)?n.base_sha:null)??a??e.base,maxUsdPerRun:e.maxUsdPerRun,maxRunsPerHour:e.maxRunsPerHour,timeoutMin:e.timeoutMin,permission:e.permission,model:e.model,pullRequests:e.pullRequests}}const Z=`machine`;function Va(e=process.env){return Object.fromEntries(Object.entries(e).filter(([e])=>!/^npm_[a-z]/.test(e)&&e!==`INIT_CWD`&&e!==`NODE`))}function Ha(e=process.env,t=ye){let n=(e.PATH??``).split(a).filter(Boolean);if(!t)return n;let r=n.findIndex(e=>/[\\/]node-gyp-bin$/.test(e));return(r>=0?n.slice(r+1):n).filter(e=>!/[\\/]_npx[\\/]/.test(e))}var Ua=class{config;state;#e=new Map;#t=new Map;#n=new Map;#r=new Map;#i;#a;#o;#s;#c;#l=!1;#u=null;#d=0;#f=null;#p=0;#m=null;#h=0;#g=!1;#_;#v=null;#y=new Map;#b=0;#x=null;#S=null;#C={day:``,done:0,failed:0,stopped:0,costMicros:0,durationMs:0};constructor(e,t={}){this.config=e,this.state=nn(),this.#i=t.out??(e=>process.stdout.write(e+`
57
- `)),this.#a=t.now??Date.now,this.#o=t.progressMs??1e4,this.#s=t.checkpointMs??18e4,this.#c=t.acpCancelGraceMs,this.#_=!!t.liveFooter}#w(e){return this.state.agents[e.id]??={}}#T(e){let t=this.#r.get(e.id);return t||this.#r.set(e.id,t=new Ut(e.api,e.oauth?()=>Pr(e,{now:this.#a}):e.token)),t}#E(e,t,n=`info`,r){let i=new Date(this.#a()).toTimeString().slice(0,8),[a,o]=La[n],s=r?()=>r:a,c=e?e.name:``,l=c?Fn(c)(c):``,u=N(),d=Rn(o(t));if(u<60){this.#i(H(`${F(i)} ${s()} ${l}${l?` `:``}${d}`,u,4).join(`
56
+ `)){let[n,a]=t.split(` `);n&&Number(a)<i&&(Gi(e,[`update-ref`,`-d`,n]),r++)}}catch{}return r}const fa=/^[a-z0-9][a-z0-9._-]{0,39}$/;function pa(e){return i(e).toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[^a-z0-9]+/,``).slice(0,40).replace(/[-.]+$/,``)||`repo`}function ma(e){let t=e.repos?.length?e.repos:e.repo?[{name:pa(e.repo),path:e.repo}]:[],n=t.find(t=>t.path===e.repo);return n?[n,...t.filter(e=>e!==n)]:t}const ha=e=>ma(e)[0]??null;function ga(e,t){if(!t)return ha(e);let n=ma(e).find(e=>e.name===t.toLowerCase());if(!n)throw Error(`this computer has no repo named ${t} (it has: ${ma(e).map(e=>e.name).join(`, `)||`none`})`);return n}function _a(e){try{return t(`git`,[`-C`,e,`config`,`--get`,`remote.origin.url`],{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()}catch{return null}}function va(e){return ma(e).map((e,t)=>{let n=_a(e.path);return{name:e.name,default:t===0,github:n?na(n):null}})}function ya(e,t,n,r=!1){let i=(n??ma(e).find(e=>e.path===t)?.name??pa(t)).toLowerCase();if(!fa.test(i))throw Error(`a repo name is 1 to 40 lowercase letters, digits, dots, dashes or underscores`);let a=ma(e).filter(e=>e.path!==t);if(a.some(e=>e.name===i))throw Error(`there's already a repo named ${i}; pick another with --name`);if(a.length>=20)throw Error(`a computer can have at most 20 repos`);return{repos:[...a,{name:i,path:t}],repo:r||!e.repo||!a.length?t:e.repo}}function ba(e,t){let n=ma(e),r=n.find(e=>e.name===t.toLowerCase());if(!r)throw Error(`no repo named ${t} here (it has: ${n.map(e=>e.name).join(`, `)||`none`})`);let i=n.filter(e=>e!==r);return{repos:i,repo:e.repo===r.path?i[0]?.path:e.repo}}const xa=(e,t)=>new Promise(n=>{if(t.aborted)return n();let r=setTimeout(i,e);r.unref?.(),t.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),t.removeEventListener(`abort`,i),n()}});var Sa=class{#e;#t=new AbortController;done;constructor(e){this.#e=e,this.done=this.#n()}stop(){this.#t.abort()}async#n(){let e=this.#t.signal,t=this.#e.sleep??xa,n,r=0;for(;!e.aborted;){let i;try{i=await this.#e.ask(n,n===void 0?0:25,e)}catch(n){if(e.aborted)return;if(n instanceof J&&n.ended||n instanceof O&&(n.status===401||n.status===403)){this.#e.onEnd?.(`signed_out`);return}r++,await t(Math.min(3e5,5e3*2**(r-1)),e);continue}if(i===null){this.#e.onEnd?.(`unsupported`);return}r=0,i.wake&&n!==void 0&&!e.aborted&&this.#e.onWake(),n=i.signature}}};const Ca=36e5,wa=process.env.BITFOLK_FAST_POLL===`1`,Ta=wa?3e3:3e4,Ea=wa?3e4:6e5,Da=wa?2e3:15e3,Oa=wa?5e3:6e4,ka=wa?1e3:5e3,Aa={session_requested:`a session you asked for`,claimable_tasks:`tasks to claim`,my_tasks:`its tasks`,new_messages:`new messages`,mentioned:`a message that tags it`,review_waiting:`a review`,briefing_due:`a briefing`,feedback_ask:`a question from Bitfolk`,setup_actions:`setup`,watch_fired:`a watch`},ja=e=>e.map(e=>Aa[e]??e.replace(/_/g,` `)).join(`, `)||`new work`;function Ma(e,t){if(e){if(e.pid)try{process.kill(-e.pid,t);return}catch{}e.kill(t)}}const Na=new WeakSet;function Pa(e,t=1e4){Ma(e,`SIGTERM`),setTimeout(()=>{e&&!Na.has(e)&&Ma(e,`SIGKILL`)},t).unref()}function Fa(e){let t=Math.round(e/1e3);return t<60?`${t}s`:`${Math.floor(t/60)}m${String(t%60).padStart(2,`0`)}s`}function Ia(e){let t=[];e.costMicros!==null&&t.push(`$${(e.costMicros/1e6).toFixed(e.costMicros>0&&e.costMicros<1e4?4:2)}`);let n=e.inputTokens+e.outputTokens;return n&&t.push(n>=1e3?`${Math.round(n/1e3)}k tokens`:`${n} tokens`),t.join(`, `)}const La={info:[()=>F(L.skip),e=>e],idle:[()=>F(L.idle),F],start:[()=>I.accent(L.dot),e=>e],ok:[()=>I.ok(L.ok),e=>e],fail:[()=>I.err(L.fail),I.err],warn:[()=>I.warn(L.warn),I.warn],progress:[()=>F(L.arrow),F]};function Ra(e,t,n){let r=(t.runStarts??[]).filter(e=>e>n-Ca).sort((e,t)=>e-t),i=e.maxRunsPerHour===void 0?1/0:e.maxRunsPerHour-r.length;return{used:r.length,left:i,freesAt:i>0?n:(r[0]??n)+Ca}}function za(e,t){if(e.command)return{runtime:`command`,command:e.command};if(t.startsWith(`custom:`)){let n=e.customTools?.find(e=>`custom:${e.name}`===t);if(!n)throw Error(`your workspace asked for ${t.slice(7)}, which isn't set up here (${w} add)`);return n.protocol===`acp`?{runtime:`acp`,acp:{...ot(n.command),agent:t}}:{runtime:`command`,command:n.command}}let n=Ze(t);if(n)return{runtime:n};let r=Kr(t);if(r)return{runtime:`acp`,acp:r};throw Error(`this runner can't start ${t}`)}function Ba(e,t){let n=t.session.retry,r=n?.mode===`fix`&&n.branch?.startsWith(`bitfolk/builder-`)?n.branch.slice(8):null,i=ga(e,t.session.repo)?.path,a=e.pullRequests&&i&&!n?aa(i,ia(i,e.base)):null;return{id:t.builder.id,name:t.builder.name,slug:r&&an(r)===r?r:an(`builder-${t.session.id.slice(-8)}`),...za(e,t.session.runtime),api:e.api,token:t.builder.token,repo:i,base:(n?.mode===`redo`&&/^[0-9a-f]{7,64}$/.test(n.base_sha??``)?n.base_sha:null)??a??e.base,maxUsdPerRun:e.maxUsdPerRun,maxRunsPerHour:e.maxRunsPerHour,timeoutMin:e.timeoutMin,permission:e.permission,model:e.model,pullRequests:e.pullRequests}}const Z=`machine`;function Va(e=process.env){return Object.fromEntries(Object.entries(e).filter(([e])=>!/^npm_[a-z]/.test(e)&&e!==`INIT_CWD`&&e!==`NODE`))}function Ha(e=process.env,t=ye){let n=(e.PATH??``).split(a).filter(Boolean);if(!t)return n;let r=n.findIndex(e=>/[\\/]node-gyp-bin$/.test(e));return(r>=0?n.slice(r+1):n).filter(e=>!/[\\/]_npx[\\/]/.test(e))}var Ua=class{config;state;#e=new Map;#t=new Map;#n=new Map;#r=new Map;#i;#a;#o;#s;#c;#l;#u=!1;#d=null;#f=0;#p=null;#m=0;#h=null;#g=0;#_=!1;#v;#y=null;#b=new Map;#x=0;#S=null;#C=null;#w={day:``,done:0,failed:0,stopped:0,costMicros:0,durationMs:0};constructor(e,t={}){this.config=e,this.state=nn(),this.#i=t.out??(e=>process.stdout.write(e+`
57
+ `)),this.#a=t.now??Date.now,this.#o=t.progressMs??1e4,this.#s=t.heartbeatMs??3e5,this.#c=t.checkpointMs??18e4,this.#l=t.acpCancelGraceMs,this.#v=!!t.liveFooter}#T(e){return this.state.agents[e.id]??={}}#E(e){let t=this.#r.get(e.id);return t||this.#r.set(e.id,t=new Ut(e.api,e.oauth?()=>Pr(e,{now:this.#a}):e.token)),t}#D(e,t,n=`info`,r){let i=new Date(this.#a()).toTimeString().slice(0,8),[a,o]=La[n],s=r?()=>r:a,c=e?e.name:``,l=c?Fn(c)(c):``,u=N(),d=Rn(o(t));if(u<60){this.#i(H(`${F(i)} ${s()} ${l}${l?` `:``}${d}`,u,4).join(`
58
58
  `));return}let f=`${F(i)} ${s()} ${l}${` `.repeat(Math.max(0,12-B(c)))} `;this.#i(H(f+d,u,B(f)).join(`
59
- `))}#D(){rn(this.state)}#O(e,t){let n=this.#a()+Math.max(1e3,t);return this.#t.set(e.id,n),n}#k(e){let t=(this.#n.get(e.id)??0)+1;return this.#n.set(e.id,t),Math.min(Ea,Da*2**(t-1))}stop(){if(!this.#l){this.#l=!0;for(let e of this.#e.values())this.#A(e,`owner`);this.#m?.()}}#A(e,t){if(e.cancel){let n=e.cancel(t);return n&&t===`owner`&&(e.stopped=!0),n}return t===`owner`&&(e.stopped=!0),Pa(e.child),!0}killAll(){for(let e of this.#e.values())e.stopped=!0,Ma(e.child,`SIGKILL`)}#j(e,t){let n=e=>{this.#l||this.#e.has(e)||(this.#t.set(e,Math.min(this.#t.get(e)??1/0,this.#a())),this.#m?.())},r=e.map(e=>new Sa({ask:(t,n,r)=>this.#T(e).wake(t,n,r),onWake:()=>n(e.id)})),i=t?this.#W():null;return i&&r.push(new Sa({ask:(e,t,n)=>i.wake(e,t,n),onWake:()=>n(Z)})),r}async#M(){if([...this.#e.values()].some(e=>e.sessionId&&!e.stopped))try{let e=await this.#W()?.peek();e&&this.#I(Z);for(let t of e?.cancel??[])this.#N(t)}catch{}}#N(e){for(let[t,n]of this.#e)if(!(n.sessionId!==e||n.stopped))return this.#E({id:t,name:`Builder`},`canceled from your workspace; stopping it`,`warn`),this.#A(n,`owner`),!0;return!1}#P(e,t){let n=[],r=null,i=JSON.stringify([null,null,null]),a=!1,o=!1,s=async()=>{let r=this.#W();if(a||o||!r)return;let s={input_tokens:t.inputTokens||null,output_tokens:t.outputTokens||null,cost_micros:t.costMicros},c=JSON.stringify(Object.values(s));if(n.length||c!==i){a=!0;try{let t=n.slice(),a=await r.progress(e,{lines:t,...s});if(a.status!==`error`){i=c;let e=new Set(t);for(;n.length&&e.has(n[0]);)n.shift()}a.status===`not_found`&&(o=!0),a.canceled&&!o&&this.#N(e)}finally{a=!1}}},c=setInterval(()=>void s(),this.#o);return{add(e){e&&e!==r&&(r=e,n.push(e),n.length>5&&n.shift())},stop(){o=!0,clearInterval(c)}}}get running(){return this.#e.size}status(){let e=[...this.#e.values()],t=e.map(e=>e.tally?.costMicros).filter(e=>typeof e==`number`),n=e.map(e=>e.startedAt).filter(e=>typeof e==`number`),r=[...this.#t.entries()].filter(([e])=>e!==Z||!this.#f).map(([,e])=>e);return{running:e.length,since:n.length?Math.min(...n):null,costMicros:t.length?t.reduce((e,t)=>e+t,0):null,nextCheckAt:this.#g||this.#h||!r.length?null:Math.min(...r),checking:this.#h>0,stopping:this.#l,sessions:e.map(e=>({name:e.name??``,tool:e.tool??``,title:e.title??``,startedAt:e.startedAt??e.joinedAt??this.#a(),costMicros:e.tally?.costMicros??null,activity:e.activity??null,activityAt:e.activityAt??null,stopping:e.stopped})).sort((e,t)=>e.startedAt-t.startedAt),queued:this.#b,waitingFor:this.#b?this.#x:null,capFreesAt:this.#S,lastCheckAt:this.#v,checkFailure:[...this.#y.values()].sort((e,t)=>e.since-t.since)[0]??null,signedOut:this.#f,runsThisHour:this.config.machine?Ra(this.config.machine,this.state.agents[Z]??{},this.#a()).used:null,today:this.#F()}}#F(){let e=new Date(this.#a()).toDateString();return this.#C.day!==e&&(this.#C={day:e,done:0,failed:0,stopped:0,costMicros:0,durationMs:0}),this.#C}#I(e){this.#v=this.#a(),this.#y.delete(e)}#L(e,t,n){let r=t.message;this.#y.set(e,{message:r,since:this.#y.get(e)?.since??this.#a(),retryAt:n})}async#R(e){this.#h++;try{return await e()}finally{this.#h--}}async check(e){let t=this.#w(e);if(t.disabledReason||this.#e.has(e.id))return null;let{left:n,freesAt:r}=Ra(e,t,this.#a());if(n<=0)return this.#E(e,`hit ${e.maxRunsPerHour} runs this hour; next check at ${new Date(r).toTimeString().slice(0,5)}`,`warn`),this.#t.set(e.id,r),null;let i;try{i=await this.#R(()=>this.#T(e).peek(t.etag))}catch(n){if(n instanceof J&&n.ended)return t.disabledReason=`its sign-in ended (unlinked on your workspace, or unused for 90 days)`,this.#E(e,`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\`.`,`fail`),this.#z(e),null;if(n instanceof O&&n.status===401){let r=n.code===`agent_removed`;return t.disabledReason=r?`its owner removed it from Bitfolk`:`token rejected (revoked, or the agent was archived)`,this.#E(e,r?`stopped: ${t.disabledReason}. Run \`${w} rm "${e.name}"\` to forget it.`:`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\` (it keeps your options).`,`fail`),this.#z(e),null}let r=this.#k(e);return this.#E(e,`peek failed (${n.message}); retrying in ${Fa(r)}`,`warn`),this.#L(e.id,n,this.#O(e,r)),null}return this.#n.delete(e.id),this.#I(e.id),i?(t.etag=i.etag,t.pollMs=i.next_poll_after_s*1e3,this.#D(),i.tier===`silent`||!i.has_work?(this.#O(e,i.next_poll_after_s*1e3),null):{done:this.#V(e,i)}):(this.#O(e,t.pollMs??Oa),null)}#z(e){this.#t.delete(e.id),this.#n.delete(e.id),this.#y.delete(e.id),this.#D()}#B(e,t,n,r,i={}){return this.#K(e,t,n,r,i).catch(t=>(n.child&&!Na.has(n.child)&&Pa(n.child),n.cancel=void 0,this.#E(e,`couldn't start its run: ${R(t?.message??String(t)).split(`
60
- `)[0]}`,`fail`),this.#w(e).etag=void 0,this.#O(e,this.#k(e)),null))}#V(e,t,n){let r={child:null,done:Promise.resolve(null),stopped:!1,sessionId:n?.session.id,name:e.name,tool:n?.session.runtime??e.acp?.agent??e.runtime,title:R(n?n.session.task?.title??n.session.goal?.title??`a goal`:`woke for ${ja(t.reasons)}`),joinedAt:this.#a()},i=this.#B(e,t,r,n);return n?.session.retry?.mode===`fix`&&(i=i.then(i=>!i?.resumed||i.status!==`failed`||i.durationMs>=2e4||r.stopped||this.#l?i:(this.#E(e,`couldn't pick up the earlier conversation; starting fresh on the same branch`,`warn`),this.#B(e,t,r,n,{fresh:!0})))),r.done=(n?i.then(t=>this.#G(e,n,t)):i).finally(()=>{this.#e.delete(e.id),this.#m?.()}),this.#e.set(e.id,r),r.done}#H(e,t,n,r){let i=``,a=null,o=()=>a??=(async()=>{let a=this.#W();if(!a)return;let o=null;try{o=la(t.dir,e)}catch{}let s={branch:t.branch??null,base_sha:n,head_sha:o?.head??null,checkpoint_sha:o?.checkpoint??null,tool_session_id:r.sessionId??null},c=JSON.stringify(s);c!==i&&await a.checkpoint(e,s)&&(i=c)})().finally(()=>{a=null}),s=setInterval(()=>void o(),this.#s);return o(),{async final(){clearInterval(s),await a,await o()},stop:()=>clearInterval(s)}}async#U(e=!1){let t=this.config.machine,n=this.#W();if(!(!t||!n||!e&&this.#d&&this.#a()-this.#d<6e5)){this.#d=this.#a();try{await n.report({usage:Fe(t.runtimes,this.state.usage,{now:this.#a()}),repos:va(t)})}catch{}}}#W(){let e=this.config.machine;return!e||this.#f?null:this.#u??=new Wt(e.api,()=>Pr(e,{now:this.#a}))}async checkMachine(){let e=this.config.machine,t=this.#W();if(!e||!t)return[];let n=this.state.agents[Z]??={},r={id:Z,name:e.name},i;try{i=await this.#R(()=>t.peek())}catch(e){if(e instanceof J&&e.ended||e instanceof O&&e.status===401)return this.#f=`this machine was unlinked, or its sign-in ended`,this.#E(r,`stopped: ${this.#f}. Run \`${w} login\` to link it again.`,`fail`),[];let t=this.#k(r);this.#E(r,`check-in failed (${e.message}); retrying in ${Fa(t)}`,`warn`);let n=this.#a()+t;return this.#t.set(Z,n),this.#L(Z,e,n),[]}this.#n.delete(r.id),this.#I(Z),await this.#U();for(let e of i.cancel??[])this.#N(e);let a=Math.max(1e3,i.next_poll_after_s*1e3);this.#t.set(Z,this.#a()+a);let o=()=>{[...this.#e.values()].some(e=>e.sessionId&&!e.stopped)&&this.#t.set(Z,Math.min(this.#t.get(Z)??1/0,this.#a()+ka))},s=[],c=i.queued;for(this.#x=null,this.#S=null;c>0&&!this.#l;c--){if(this.#e.size>=this.config.maxConcurrent){this.#x=`slot`;break}let{left:i,freesAt:a}=Ra(e,n,this.#a());if(i<=0){this.#x=`cap`,this.#S=a,this.#p!==a&&(this.#p=a,this.#E(r,`hit ${e.maxRunsPerHour} sessions this hour; the rest wait until ${new Date(a).toTimeString().slice(0,5)}`,`warn`)),this.#t.set(Z,a);break}let o;try{o=await this.#R(()=>t.claim({runtimes:e.runtimes,timeout_min:e.timeoutMin??600}))}catch(e){this.#E(r,`couldn't take a session (${e.message})`,`warn`);break}if(!o){c=0;break}this.#b=Math.max(0,this.#b-1),n.runStarts=[...(n.runStarts??[]).filter(e=>e>this.#a()-Ca),this.#a()],this.#D();let l;try{l=Ba(e,o)}catch(e){this.#E(r,`can't run a session: ${e.message}`,`fail`),await t.finish(o.session.id,{status:`failed`,summary:null,error:e.message}).catch(()=>{});continue}let u=R(o.session.task?.title??o.session.goal?.title??`a goal`);this.#E(l,`started a session: ${I.bold(u)} ${F(`·`)} ${I.tool(z(Y(o.session.runtime)))}`,`start`,Wi(o.session.runtime));let d=await new Ut(l.api,l.token).peek().catch(()=>null),f={has_work:!0,tier:`full`,reasons:[`session_requested`,...(d?.reasons??[]).filter(e=>e!==`setup_actions`)],briefing_goal_id:o.session.goal?.id??null,etag:d?.etag??``,next_poll_after_s:60,rules:d?.rules};s.push({done:this.#V(l,f,o)})}return this.#b=this.#x?Math.max(0,c):0,o(),s}async#G(e,t,n){let r=n?.status===`succeeded`?`succeeded`:n?.status===`interrupted`?`canceled`:`failed`,i=null;if(n){let t=n.work?.startSha??null,r={commits:[],commit_count:0,files_changed:0,insertions:0,deletions:0};if(n.work?.branch&&t)try{e.repo&&Ki(n.work.dir,e.repo),r=ta(n.work.dir,t,n.work.branch)}catch{}if(i={base:t?.slice(0,12)??null,...r,duration_ms:n.durationMs},e.pullRequests&&e.repo&&n.status===`succeeded`&&r.commit_count>0&&n.work?.branch){let t=oa(e.repo,n.work.branch,e.base);`error`in t?(i.push_error=t.error,this.#E(e,`couldn't push ${I.tool(n.work.branch)} (${t.error}); it stays here for you`,`warn`)):(i.push=t,this.#E(e,`pushed ${I.tool(n.work.branch)} to ${t.repo}; a pull request into ${t.base} waits for your approval`,`ok`))}}try{await this.#W()?.finish(t.session.id,{status:r,summary:n?.tally.summary??null,error:n?n.status===`succeeded`?null:n.tally.error??n.status:`the session couldn't start on this machine`,branch:n?.work?.branch??null,result:i,input_tokens:n?n.tally.inputTokens:null,output_tokens:n?n.tally.outputTokens:null,cost_micros:n?.tally.costMicros??null,model:((e.runtime===`acp`?n?.tally.model??e.model:e.model??n?.tally.model)??null)?.slice(0,100)??null}),i?.commit_count&&this.#E(e,`left ${I.bold(`${i.commit_count} commit${i.commit_count===1?``:`s`}`)} on ${I.tool(n.work.branch)} ${F(`(${i.files_changed} files)`)}`,`ok`)}catch(t){this.#E(e,`couldn't report the session's end (${t.message}); Bitfolk times it out`,`warn`)}let a=n?n.tally.limit??Pe(n.tally.error,this.#a()):null;return a&&(this.state.usage={...this.state.usage,[t.session.runtime]:{...a,at:this.#a()}},a.limited_until&&this.#E(e,`${Y(t.session.runtime)} hit its usage limit; sessions go to another tool until ${new Date(a.limited_until).toTimeString().slice(0,5)}`,`warn`)),this.#U(!0),this.#E(e,`session ended`,`idle`),$i(e),e.repo&&da(e.repo),delete this.state.agents[e.id],this.#D(),n}async#K(e,t,r,i,o={}){let s=this.#w(e),l=this.#T(e),u=Xe[e.runtime],d=e.runtime===`acp`?e.acp:void 0;if(e.runtime===`acp`&&!d)throw Error(`${e.name} has runtime acp but no ACP command`);let f=this.#a(),m;try{m=Xi(e)}catch(t){return s.etag=void 0,this.#E(e,`can't prepare its workspace: ${t.message}`,`fail`),this.#O(e,this.#k(e)),null}let h=m.branch?ea(m.dir):null,_=i?.session.retry??null,v=!!(_?.mode===`fix`&&m.created&&m.branch&&_.checkpoint_sha&&ua(m.dir,_.checkpoint_sha));v&&this.#E(e,`put back the earlier session's uncommitted work on ${I.tool(String(m.branch))}`,`ok`);let y=null;try{y=await l.runStart({client_run_id:ge(),host:de(),runtime:d?d.agent:e.runtime,reasons:t.reasons,tier:t.tier})}catch(n){if(n instanceof O&&n.status===409)return this.#E(e,`is paused by its owner; not starting`,`idle`),this.#O(e,t.next_poll_after_s*1e3||(wa?Oa:6e5)),null;this.#E(e,`couldn't record the run start (${n.message}); running anyway`,`warn`)}y||await l.status(`working`).catch(()=>{});let ee=o.fresh?null:_?.mode===`fix`?u.resumes!==!1&&_.tool_session_id?_.tool_session_id:null:u.resumes!==!1&&!m.created&&s.sessionId&&s.sessionAt&&f-s.sessionAt<432e5?s.sessionId:null,b=!!m.branch&&!!u.runnerCommits?.(e),te=ri(t,{agentName:e.name,api:e.api,branch:m.branch,runnerCommits:b,resumed:!!ee,session:i?{goal:i.session.goal?.title??null,task:i.session.task?.title??null,note:i.session.note,goalDone:i.session.goal?.done_definition??null,taskSpec:i.session.task?.spec??null,asks:i.session.asks??[],requestedBy:i.session.requested_by??`owner`,requestedByAgent:i.session.requested_by_agent??null,retry:_?{mode:_.mode,branch:_.branch,summary:_.summary,error:_.error,status:_.status,restored:v}:void 0,tool:i.session.runtime,repo:this.config.machine?ma(this.config.machine).find(t=>t.path===e.repo)?.name??null:null,computer:i.session.computer??null}:void 0}),ne=u.launch(e,te,ee);s.runStarts=[...(s.runStarts??[]).filter(e=>e>f-Ca),f],this.#D(),this.#E(e,`woke ${F(`for ${ja(t.reasons)}`)} → ${I.tool(z(d?`${Y(d.agent)} over ACP`:e.runtime))}${ee?F(`, resuming`):``}`,`start`);let re=c(k(),`logs`,e.slug);x(re,{recursive:!0,mode:448});let ie=c(re,`${new Date(f).toISOString().replace(/[:.]/g,`-`)}.jsonl`),ae=new Set([e.token]),oe=e=>[...ae].reduce((e,t)=>e.split(t).join(`[token]`),e),se=(e,t)=>g(ie,JSON.stringify({t:this.#a(),stream:e,line:oe(t)})+`
61
- `,{mode:384}),ce=Br(),le=Fr(e),ue=e.oauth?setInterval(()=>{e.oauth.expiresAt-this.#a()>6e5||Pr(e,{now:this.#a}).then(()=>{ae.add(e.token),Fr(e)}).catch(t=>this.#E(e,`couldn't renew its sign-in mid-run: ${t.message}`,`warn`))},6e4):null,C=Le();r.tally=C,r.startedAt=this.#a();let fe={...Va(),PATH:[ce,...Ha(),...Hr()].join(a),...d?{NO_BROWSER:`1`}:{BITFOLK_TOKEN:e.token},BITFOLK_TOKEN_FILE:le,BITFOLK_API:e.api.replace(/\/+$/,``),BITFOLK_AGENT:e.name,...y?{BITFOLK_RUN_ID:y.id}:{},...i?{BITFOLK_SESSION_ID:i.session.id}:{}};d&&delete fe.BITFOLK_TOKEN;for(let[e,t]of Object.entries(fe))t&&t.length>=12&&/KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH/i.test(e)&&e!==`BITFOLK_TOKEN_FILE`&&ae.add(t);let pe=i?this.#P(i.session.id,C):null,me=i&&m.branch?this.#H(i.session.id,m,h,C):null,he=null,_e=t=>{let n=t&&R(oe(t)).trim();pe?.add(n),n&&n!==he&&(he=n,r.activity=n,r.activityAt=this.#a(),this.#E(e,n,`progress`))},ve=null,w=null,ye=!1,be,xe=null;try{be=await new Promise(t=>{let i=n(ne.bin,ne.args,{cwd:m.dir,env:fe,stdio:[`pipe`,`pipe`,`pipe`],detached:!0});if(r.child=i,i.stdin.on(`error`,()=>{}),d){let t=new It({stdin:i.stdin,stdout:i.stdout,agent:{...e,acp:d},label:Y(d.agent),cwd:m.dir,home:k(),packet:te,tally:C,clientVersion:Rt,log:se,activity:_e,kill:()=>{Na.has(i)||Pa(i)},cancelGraceMs:this.#c});r.cancel=e=>t.cancel(e),xe=t.run()}else i.stdin.end(ne.stdin),p({input:i.stdout}).on(`line`,e=>{se(`out`,e),u.parse(e,C),u.activity&&_e(u.activity(e,m.dir))});(this.#l||r.stopped)&&this.#A(r,`owner`),p({input:i.stderr}).on(`line`,e=>{se(`err`,e),e.trim()&&(w=oe(e.trim()).slice(0,300))});let a=e.timeoutMin===void 0?void 0:setTimeout(()=>{ye=!0,this.#A(r,`timeout`)||(ye=!1)},e.timeoutMin*6e4);i.on(`error`,e=>{ve=e.code===`ENOENT`?`${ne.bin} isn't installed or isn't on PATH`:e.message}),i.on(`close`,e=>{Na.add(i),clearTimeout(a),t({code:e})})})}finally{pe?.stop(),me?.stop(),ue&&clearInterval(ue),S(le,{force:!0})}let Se=xe?await xe:null;r.cancel=void 0,u.afterExit?.(ne,C),S(le,{force:!0});let Ce=this.#a()-f,T,we;if(Se?(T=r.stopped?`interrupted`:C.overBudget?`over_budget`:ve||ye||!Se.ok?`failed`:`succeeded`,we=ve??(T===`over_budget`?C.error:ye&&T===`failed`?`stopped after ${e.timeoutMin} minutes`:T===`failed`?Se.exited?w??`${Se.error??`the tool exited`}${be.code===null?``:` (exit ${be.code})`}`:Se.error??w??`the turn failed`:null),T===`failed`&&(C.error=we)):(T=r.stopped?`interrupted`:C.overBudget?`over_budget`:ve||ye||be.code!==0||C.isError?`failed`:`succeeded`,we=ve??(ye?`stopped after ${e.timeoutMin} minutes`:T===`failed`?C.error??w??`exit ${be.code}`:null)),T===`failed`&&!C.error&&(C.error=we),b&&!ve){let t=`${e.name}: ${C.summary??`work from a Bitfolk run`}`,n=`Bitfolk run${y?` ${y.id}`:``}, ${T}.`;try{e.repo&&m.branch&&Ki(m.dir,e.repo);let r=Zi(m.dir,`${t.length>72?t.slice(0,71)+`…`:t}\n\n${n}`);r&&this.#E(e,`committed its changes as ${I.bold(r)} on ${I.tool(String(m.branch))}`,`ok`)}catch(t){this.#E(e,`couldn't commit its changes: ${t.message.split(`
62
- `)[0]}`,`warn`)}}await me?.final().catch(()=>{}),T===`failed`&&ee&&Ce<2e4?(s.sessionId=void 0,s.etag=void 0):C.sessionId&&(s.sessionId=C.sessionId,s.sessionAt=this.#a()),(T===`failed`||T===`interrupted`)&&(s.etag=void 0),this.#D();let Te={status:T,exit_code:Se?Se.ok&&T===`succeeded`?0:null:be.code,session_id:C.sessionId,input_tokens:C.inputTokens,output_tokens:C.outputTokens,cost_micros:C.costMicros,summary:C.summary,error:we};y?await l.runFinish(y.id,Te).catch(async()=>{await new Promise(e=>setTimeout(e,2e3)),await l.runFinish(y.id,Te).catch(t=>this.#E(e,`couldn't record the run end: ${t.message}`,`warn`))}):await l.status(`idle`).catch(()=>{});let Ee=this.#F();T===`succeeded`?Ee.done++:T===`interrupted`?Ee.stopped++:Ee.failed++,Ee.costMicros+=C.costMicros??0,Ee.durationMs+=Ce;let De=Ia(C),Oe=T===`succeeded`?I.ok(I.bold(T)):T===`failed`?I.err(I.bold(T)):I.warn(I.bold(T));return this.#E(e,[`${Oe} in ${Fa(Ce)}`,...De?De.split(`, `).map(e=>e.startsWith(`$`)?I.money(e):e):[]].join(` ${F(`·`)} `)+`${we?` ${I.err(`(${R(we)})`)}`:``}${C.summary?` ${F(`—`)} ${R(C.summary)}`:``}`,T===`succeeded`?`ok`:T===`failed`?`fail`:`warn`),T===`failed`?this.#O(e,this.#k(e)):(this.#n.delete(e.id),this.#O(e,Ta)),{agent:e.name,status:T,tally:C,durationMs:Ce,work:{dir:m.dir,branch:m.branch,startSha:h},resumed:!!ee}}async run(e={}){this.#g=!!e.once;let t=this.config.agents.filter(e=>!this.#w(e).disabledReason);for(let e of this.config.agents){let t=this.#w(e).disabledReason;t&&this.#E(e,`skipped: ${t}`,`idle`)}let n=this.config.machine;if(t.length===0&&!n)return this.#E(null,`nothing to run. Link this computer with \`${w} login\`.`,`warn`),[];if(e.once){let e=[];if(n){let t=await this.checkMachine();t.length===0&&!this.#f&&this.#E({name:n.name},`no sessions waiting`,`idle`),e.push(...t.map(e=>e.done))}for(let n of t){for(;this.#e.size>=this.config.maxConcurrent;)await Promise.race([...this.#e.values()].map(e=>e.done));let t=await this.check(n);t?e.push(t.done):this.#e.has(n.id)||this.#E(n,`nothing to do`,`idle`)}let r=n?setInterval(()=>void this.#M(),ka):null;try{return(await Promise.all(e)).filter(e=>e!==null)}finally{r&&clearInterval(r)}}let r=[],i=[n?`${n.name} for sessions`:null,t.length?`${t.length} agent${t.length===1?``:`s`}`:null].filter(Boolean).join(` and `);this.#E(null,`watching ${I.bold(i)} ${F(`(up to ${this.config.maxConcurrent} at once).${this.#_?``:` Ctrl-C to stop.`}`)}`,`start`);let a=this.#j(t,!!n);for(;!this.#l;){let e=this.#a();if(n&&!this.#f&&(this.#t.get(Z)??0)<=e)for(let e of await this.checkMachine())e.done.then(e=>e&&r.push(e));for(let n of t){if(this.#l||this.#e.size>=this.config.maxConcurrent)break;this.#e.has(n.id)||this.#w(n).disabledReason||(this.#t.get(n.id)??0)>e||(await this.check(n))?.done.then(e=>e&&r.push(e))}let i=Math.min(...t.filter(e=>!this.#w(e).disabledReason).map(t=>this.#t.get(t.id)??e),n&&!this.#f?this.#t.get(Z)??e:1/0),a=Math.max(1e3,Math.min(6e4,i-this.#a()));await new Promise(e=>{let t=setTimeout(e,a);this.#m=()=>{clearTimeout(t),e()}}),this.#m=null}for(let e of a)e.stop();return await Promise.all([...this.#e.values()].map(e=>e.done)),r}};const Wa=e=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e);function Ga(e){for(let t=0;t<20;t++){let t;try{t=ie(e,`utf8`).trim()}catch{return null}if(t)return Number(t)||0;Wa(25)}return 0}function Ka(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function qa(){let e=Ga(c(k(),`runner.pid`));return e&&Ka(e)?e:null}function Ja(){let e=c(k(),`runner.pid`);x(k(),{recursive:!0,mode:448});let t=String(process.pid),n=()=>{try{ie(e,`utf8`).trim()===t&&S(e,{force:!0})}catch{}};for(let r=0;r<3;r++){try{return ue(e,t,{mode:384,flag:`wx`}),n}catch(e){if(e.code!==`EEXIST`)throw e}let r=Ga(e);if(r===null)continue;if(Ka(r))throw Error(`a runner is already going (pid ${r}). Stop it first, or see \`${w} status\`.`);let i=`${e}.${process.pid}.tmp`;if(ue(i,t,{mode:384}),ce(i,e),Wa(25),Ga(e)===process.pid)return n}throw Error(`another runner is starting at the same moment. Try again, or see \`${w} status\`.`)}var Q=class extends Error{};const $=e=>e.slice(-8);function Ya(e){let t=e.trim(),n=t.split(/\r?\n/)[0].trim();if(n===t&&n.length<=200)return{title:n,note:``};let r=n;if(r.length>200){let e=r.slice(0,199),t=e.lastIndexOf(` `);r=`${(t>80?e.slice(0,t):e).trimEnd()}…`}return{title:r,note:t.slice(0,2e3)}}function Xa(e,t){if(!e)return;let n=e.trim().toLowerCase();for(let e of[n,n.replace(/-/g,`_`),Yr(n)])if(t.runtimes.includes(e))return e;throw new Q(`${e} isn't on for this computer (on: ${t.runtimes.map(e=>e.startsWith(`custom:`)?e.slice(7):e.replace(/_/g,`-`)).join(`, `)}). Turn it on with bitfolk add ${e}`)}const Za=e=>e===null?null:`$${(e/1e6).toFixed(e>0&&e<1e4?4:2)}`,Qa={queued:e=>F(`${L.idle} ${e}`),running:e=>I.sky(`${L.dot} ${e}`),succeeded:e=>I.ok(`${L.ok} ${e}`),failed:e=>I.err(`${L.fail} ${e}`),canceled:e=>F(`${L.skip} ${e}`)},$a=e=>(Qa[e]??(e=>e))(e===`succeeded`?`done`:e);function eo(e,t=Date.now()){if(!e)return``;let n=Math.max(0,Math.round((t-Date.parse(e))/1e3));return n<60?`just now`:n<3600?`${Math.floor(n/60)}m ago`:n<86400?`${Math.floor(n/3600)}h ago`:`${Math.floor(n/86400)}d ago`}function to(e,t=Date.now()){return e.started_at?ir((e.finished_at?Date.parse(e.finished_at):t)-Date.parse(e.started_at)):null}const no=e=>R(e.task?.title??e.goal?.title??`a goal`);function ro(e,t=Date.now()){if(!e.length)return[K(`${F(L.idle)} No sessions on this computer yet. Start one with ${U(`${w} run "<what to do>"`)}.`)];let n=N();return Kn(e.map(e=>{let r=Za(e.cost_micros),i=e.status===`running`?`${to(e,t)??``}`:e.status===`queued`?eo(e.created_at,t):eo(e.finished_at??e.created_at,t),a=e.status===`running`&&e.last?e.last:e.status===`failed`&&e.error?e.error:!e.task&&e.note?e.note:null,o=a?F(` · ${R(a)}`):``;return[I.tool($(e.id)),$a(e.status),F(z(Y(e.runtime))),V(`${P(no(e))}${o}`,Math.max(20,n-52)),F(z([i,r].filter(Boolean).join(` · `)))]}),{indent:2,gap:2,max:n})}function io(e,t={}){let n=t.now??Date.now(),r=[];r.push(K(`${I.tool($(e.id))} ${$a(e.status)} ${P(no(e))}`));let i=[];e.task&&e.goal&&i.push([`goal`,R(e.goal.title)]),i.push([`tool`,Y(e.runtime)+(e.model?F(` · ${R(e.model)}`):``)]),e.note&&i.push([`note`,R(e.note)]),e.retry_mode&&i.push([`retry`,`${e.retry_mode} of ${$(e.retry_of??``)}`]);let a=to(e,n);i.push([`started`,e.started_at?`${eo(e.started_at,n)}${a?F(` · ran ${a}`):``}`:F(`waiting since ${eo(e.created_at,n)}`)]);let o=(e.input_tokens??0)+(e.output_tokens??0),s=Za(e.cost_micros);(s||o)&&i.push([`spent`,[s?I.money(s):null,o?F(`${Math.round(o/1e3)}k tokens`):null].filter(Boolean).join(F(` · `))]),e.repo&&i.push([`repo`,R(e.repo)]),e.branch&&i.push([`branch`,I.tool(e.branch)]);let c=e.result?.pull_request;if(c?.url){let e=c.state===`merged`?I.ok(`merged`):c.state===`failed`?I.err(`merge failed: ${R(c.error??``)}`):F(`waits for your approval to merge`);i.push([`pull request`,`${R(c.url)} ${e}`])}else e.result?.push?i.push([`pull request`,F(`pushed to ${R(e.result.push.repo)}; opening one needs GitHub connected in Bitfolk`)]):e.result?.push_error&&i.push([`pull request`,I.warn(`not pushed: ${R(e.result.push_error)}`)]);r.push(...Jn(i,2));let l=e.progress??[];if(l.length){r.push(``);for(let e of l)r.push(K(` ${F(ao(e.at))} ${R(e.text)}`,12))}let u=e.result?.commits??[];if(u.length){r.push(``,` ${P(`${e.result.commit_count} commit${e.result.commit_count===1?``:`s`}`)}${F(`, ${e.result.files_changed} files, +${e.result.insertions} -${e.result.deletions}`)}`);for(let e of u.slice(0,10))r.push(K(` ${I.tool(e.sha.slice(0,7))} ${R(e.subject)}`,10))}return e.summary&&r.push(``,K(W(R(e.summary)))),e.error&&e.status!==`succeeded`&&r.push(``,K(`${I.err(L.fail)} ${R(e.error)}`)),t.localLog&&r.push(``,K(F(`The tool's full output on this computer: ${t.localLog}`))),r}const ao=e=>new Date(e).toTimeString().slice(0,8);function oo(e){return e.length?Kn(e.map(e=>[I.tool($(e.id)),P(R(e.name??e.title)),F(z(e.tasks?`${e.open_tasks} of ${e.tasks} tasks open`:`no tasks yet`)),e.status===`paused`?I.warn(`paused`):``]),{indent:2,gap:2,max:N()}):[K(`${F(L.idle)} No open goals. ${U(`${w} run "<what to do>"`)} starts one.`)]}function so(e){let t=c(k(),`logs`,`builder-${$(e)}`);return b(t)?t:null}function co(e){let t=e.trim().toLowerCase();if(!/^[0-9a-f-]{4,36}$/.test(t))throw new Q(`"${e}" isn't a session id. Give one, or its last 4 or more characters, as ${w} sessions lists them`);return t}function lo(e){if(!e)throw new Q(`link this computer first: ${w} login`);return{m:e,api:new Wt(e.api,()=>Pr(e))}}function uo(e){throw e instanceof O&&[400,404,429].includes(e.status)&&e.message!==`invalid_body`?new Q(e.message):e}async function fo(e,t,n,r){let{m:i,api:a}=lo(e),o=t.join(` `).trim();if(!o&&!n.goal)throw new Q(`say what to do: ${w} run "fix the flaky login test", or name a goal with --goal`);if(n.task&&!n.goal)throw new Q(`--task goes with --goal: ${w} run --goal <goal> --task <task>`);let s=Xa(n.runtime,i)??null,c=n.in?ga(i,n.in).name:null,l=n.goal?{goal:n.goal,task:n.task??null,note:o.slice(0,2e3)}:{...Ya(o),goal:null},u=await q(n.goal?`Starting a session`:`Starting a goal`,()=>a.start({...l,runtime:s,repo:c})).catch(uo),d=u.session,f=R(d.goal?.title??``);if(console.log(K(W(`${u.goal_created?`New goal`:`Goal`} ${P(f)} ${F(`(${$(d.goal?.id??``)})`)}`))),console.log(K(`${I.ok(L.ok)} Session ${I.tool($(d.id))} ${F(`on`)} ${Y(d.runtime)}${d.repo?F(` in ${R(d.repo)}`):``}${d.task?F(` for ${R(d.task.title)}`):``}${u.ahead?F(` · ${u.ahead} waiting before it`):``}`)),n.detach)console.log(K(F(r.upRunning?`${w} up takes it here. Follow it with ${w} logs ${$(d.id)} --follow.`:`It waits until ${w} up runs on this computer.`)));else if(r.upRunning)console.log(K(F(`${w} up on this computer takes it. Following along; Ctrl-C stops following, not the session.`))+`
59
+ `))}#O(){rn(this.state)}#k(e,t){let n=this.#a()+Math.max(1e3,t);return this.#t.set(e.id,n),n}#A(e){let t=(this.#n.get(e.id)??0)+1;return this.#n.set(e.id,t),Math.min(Ea,Da*2**(t-1))}stop(){if(!this.#u){this.#u=!0;for(let e of this.#e.values())this.#j(e,`owner`);this.#h?.()}}#j(e,t){if(e.cancel){let n=e.cancel(t);return n&&t===`owner`&&(e.stopped=!0),n}return t===`owner`&&(e.stopped=!0),Pa(e.child),!0}killAll(){for(let e of this.#e.values())e.stopped=!0,Ma(e.child,`SIGKILL`)}#M(e,t){let n=e=>{this.#u||this.#e.has(e)||(this.#t.set(e,Math.min(this.#t.get(e)??1/0,this.#a())),this.#h?.())},r=e.map(e=>new Sa({ask:(t,n,r)=>this.#E(e).wake(t,n,r),onWake:()=>n(e.id)})),i=t?this.#G():null;return i&&r.push(new Sa({ask:(e,t,n)=>i.wake(e,t,n),onWake:()=>n(Z)})),r}async#N(){if([...this.#e.values()].some(e=>e.sessionId&&!e.stopped))try{let e=await this.#G()?.peek();e&&this.#L(Z);for(let t of e?.cancel??[])this.#P(t)}catch{}}#P(e){for(let[t,n]of this.#e)if(!(n.sessionId!==e||n.stopped))return this.#D({id:t,name:`Builder`},`canceled from your workspace; stopping it`,`warn`),this.#j(n,`owner`),!0;return!1}#F(e,t){let n=[],r=null,i=JSON.stringify([null,null,null]),a=!1,o=!1,s=this.#a(),c=async()=>{let r=this.#G();if(a||o||!r)return;let c={input_tokens:t.inputTokens||null,output_tokens:t.outputTokens||null,cost_micros:t.costMicros},l=JSON.stringify(Object.values(c));if(!(!n.length&&l===i&&this.#a()-s<this.#s)){a=!0;try{let t=n.slice(),a=await r.progress(e,{lines:t,...c});if(a.status!==`error`){i=l,s=this.#a();let e=new Set(t);for(;n.length&&e.has(n[0]);)n.shift()}a.status===`not_found`&&(o=!0),a.canceled&&!o&&this.#P(e)}finally{a=!1}}},l=setInterval(()=>void c(),this.#o);return{add(e){e&&e!==r&&(r=e,n.push(e),n.length>5&&n.shift())},stop(){o=!0,clearInterval(l)}}}get running(){return this.#e.size}status(){let e=[...this.#e.values()],t=e.map(e=>e.tally?.costMicros).filter(e=>typeof e==`number`),n=e.map(e=>e.startedAt).filter(e=>typeof e==`number`),r=[...this.#t.entries()].filter(([e])=>e!==Z||!this.#p).map(([,e])=>e);return{running:e.length,since:n.length?Math.min(...n):null,costMicros:t.length?t.reduce((e,t)=>e+t,0):null,nextCheckAt:this.#_||this.#g||!r.length?null:Math.min(...r),checking:this.#g>0,stopping:this.#u,sessions:e.map(e=>({name:e.name??``,tool:e.tool??``,title:e.title??``,startedAt:e.startedAt??e.joinedAt??this.#a(),costMicros:e.tally?.costMicros??null,activity:e.activity??null,activityAt:e.activityAt??null,stopping:e.stopped})).sort((e,t)=>e.startedAt-t.startedAt),queued:this.#x,waitingFor:this.#x?this.#S:null,capFreesAt:this.#C,lastCheckAt:this.#y,checkFailure:[...this.#b.values()].sort((e,t)=>e.since-t.since)[0]??null,signedOut:this.#p,runsThisHour:this.config.machine?Ra(this.config.machine,this.state.agents[Z]??{},this.#a()).used:null,today:this.#I()}}#I(){let e=new Date(this.#a()).toDateString();return this.#w.day!==e&&(this.#w={day:e,done:0,failed:0,stopped:0,costMicros:0,durationMs:0}),this.#w}#L(e){this.#y=this.#a(),this.#b.delete(e)}#R(e,t,n){let r=t.message;this.#b.set(e,{message:r,since:this.#b.get(e)?.since??this.#a(),retryAt:n})}async#z(e){this.#g++;try{return await e()}finally{this.#g--}}async check(e){let t=this.#T(e);if(t.disabledReason||this.#e.has(e.id))return null;let{left:n,freesAt:r}=Ra(e,t,this.#a());if(n<=0)return this.#D(e,`hit ${e.maxRunsPerHour} runs this hour; next check at ${new Date(r).toTimeString().slice(0,5)}`,`warn`),this.#t.set(e.id,r),null;let i;try{i=await this.#z(()=>this.#E(e).peek(t.etag))}catch(n){if(n instanceof J&&n.ended)return t.disabledReason=`its sign-in ended (unlinked on your workspace, or unused for 90 days)`,this.#D(e,`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\`.`,`fail`),this.#B(e),null;if(n instanceof O&&n.status===401){let r=n.code===`agent_removed`;return t.disabledReason=r?`its owner removed it from Bitfolk`:`token rejected (revoked, or the agent was archived)`,this.#D(e,r?`stopped: ${t.disabledReason}. Run \`${w} rm "${e.name}"\` to forget it.`:`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\` (it keeps your options).`,`fail`),this.#B(e),null}let r=this.#A(e);return this.#D(e,`peek failed (${n.message}); retrying in ${Fa(r)}`,`warn`),this.#R(e.id,n,this.#k(e,r)),null}return this.#n.delete(e.id),this.#L(e.id),i?(t.etag=i.etag,t.pollMs=i.next_poll_after_s*1e3,this.#O(),i.tier===`silent`||!i.has_work?(this.#k(e,i.next_poll_after_s*1e3),null):{done:this.#H(e,i)}):(this.#k(e,t.pollMs??Oa),null)}#B(e){this.#t.delete(e.id),this.#n.delete(e.id),this.#b.delete(e.id),this.#O()}#V(e,t,n,r,i={}){return this.#q(e,t,n,r,i).catch(t=>(n.child&&!Na.has(n.child)&&Pa(n.child),n.cancel=void 0,this.#D(e,`couldn't start its run: ${R(t?.message??String(t)).split(`
60
+ `)[0]}`,`fail`),this.#T(e).etag=void 0,this.#k(e,this.#A(e)),null))}#H(e,t,n){let r={child:null,done:Promise.resolve(null),stopped:!1,sessionId:n?.session.id,name:e.name,tool:n?.session.runtime??e.acp?.agent??e.runtime,title:R(n?n.session.task?.title??n.session.goal?.title??`a goal`:`woke for ${ja(t.reasons)}`),joinedAt:this.#a()},i=this.#V(e,t,r,n);return n?.session.retry?.mode===`fix`&&(i=i.then(i=>!i?.resumed||i.status!==`failed`||i.durationMs>=2e4||r.stopped||this.#u?i:(this.#D(e,`couldn't pick up the earlier conversation; starting fresh on the same branch`,`warn`),this.#V(e,t,r,n,{fresh:!0})))),r.done=(n?i.then(t=>this.#K(e,n,t)):i).finally(()=>{this.#e.delete(e.id),this.#h?.()}),this.#e.set(e.id,r),r.done}#U(e,t,n,r){let i=``,a=null,o=()=>a??=(async()=>{let a=this.#G();if(!a)return;let o=null;try{o=la(t.dir,e)}catch{}let s={branch:t.branch??null,base_sha:n,head_sha:o?.head??null,checkpoint_sha:o?.checkpoint??null,tool_session_id:r.sessionId??null},c=JSON.stringify(s);c!==i&&await a.checkpoint(e,s)&&(i=c)})().finally(()=>{a=null}),s=setInterval(()=>void o(),this.#c);return o(),{async final(){clearInterval(s),await a,await o()},stop:()=>clearInterval(s)}}async#W(e=!1){let t=this.config.machine,n=this.#G();if(!(!t||!n||!e&&this.#f&&this.#a()-this.#f<6e5)){this.#f=this.#a();try{await n.report({usage:Fe(t.runtimes,this.state.usage,{now:this.#a()}),repos:va(t)})}catch{}}}#G(){let e=this.config.machine;return!e||this.#p?null:this.#d??=new Wt(e.api,()=>Pr(e,{now:this.#a}))}async checkMachine(){let e=this.config.machine,t=this.#G();if(!e||!t)return[];let n=this.state.agents[Z]??={},r={id:Z,name:e.name},i;try{i=await this.#z(()=>t.peek())}catch(e){if(e instanceof J&&e.ended||e instanceof O&&e.status===401)return this.#p=`this machine was unlinked, or its sign-in ended`,this.#D(r,`stopped: ${this.#p}. Run \`${w} login\` to link it again.`,`fail`),[];let t=this.#A(r);this.#D(r,`check-in failed (${e.message}); retrying in ${Fa(t)}`,`warn`);let n=this.#a()+t;return this.#t.set(Z,n),this.#R(Z,e,n),[]}this.#n.delete(r.id),this.#L(Z),await this.#W();for(let e of i.cancel??[])this.#P(e);let a=Math.max(1e3,i.next_poll_after_s*1e3);this.#t.set(Z,this.#a()+a);let o=()=>{[...this.#e.values()].some(e=>e.sessionId&&!e.stopped)&&this.#t.set(Z,Math.min(this.#t.get(Z)??1/0,this.#a()+ka))},s=[],c=i.queued;for(this.#S=null,this.#C=null;c>0&&!this.#u;c--){if(this.#e.size>=this.config.maxConcurrent){this.#S=`slot`;break}let{left:i,freesAt:a}=Ra(e,n,this.#a());if(i<=0){this.#S=`cap`,this.#C=a,this.#m!==a&&(this.#m=a,this.#D(r,`hit ${e.maxRunsPerHour} sessions this hour; the rest wait until ${new Date(a).toTimeString().slice(0,5)}`,`warn`)),this.#t.set(Z,a);break}let o;try{o=await this.#z(()=>t.claim({runtimes:e.runtimes,timeout_min:e.timeoutMin??null}))}catch(e){this.#D(r,`couldn't take a session (${e.message})`,`warn`);break}if(!o){c=0;break}this.#x=Math.max(0,this.#x-1),n.runStarts=[...(n.runStarts??[]).filter(e=>e>this.#a()-Ca),this.#a()],this.#O();let l;try{l=Ba(e,o)}catch(e){this.#D(r,`can't run a session: ${e.message}`,`fail`),await t.finish(o.session.id,{status:`failed`,summary:null,error:e.message}).catch(()=>{});continue}let u=R(o.session.task?.title??o.session.goal?.title??`a goal`);this.#D(l,`started a session: ${I.bold(u)} ${F(`·`)} ${I.tool(z(Y(o.session.runtime)))}`,`start`,Wi(o.session.runtime));let d=await new Ut(l.api,l.token).peek().catch(()=>null),f={has_work:!0,tier:`full`,reasons:[`session_requested`,...(d?.reasons??[]).filter(e=>e!==`setup_actions`)],briefing_goal_id:o.session.goal?.id??null,etag:d?.etag??``,next_poll_after_s:60,rules:d?.rules};s.push({done:this.#H(l,f,o)})}return this.#x=this.#S?Math.max(0,c):0,o(),s}async#K(e,t,n){let r=n?.status===`succeeded`?`succeeded`:n?.status===`interrupted`?`canceled`:`failed`,i=null;if(n){let t=n.work?.startSha??null,r={commits:[],commit_count:0,files_changed:0,insertions:0,deletions:0};if(n.work?.branch&&t)try{e.repo&&Ki(n.work.dir,e.repo),r=ta(n.work.dir,t,n.work.branch)}catch{}if(i={base:t?.slice(0,12)??null,...r,duration_ms:n.durationMs},e.pullRequests&&e.repo&&n.status===`succeeded`&&r.commit_count>0&&n.work?.branch){let t=oa(e.repo,n.work.branch,e.base);`error`in t?(i.push_error=t.error,this.#D(e,`couldn't push ${I.tool(n.work.branch)} (${t.error}); it stays here for you`,`warn`)):(i.push=t,this.#D(e,`pushed ${I.tool(n.work.branch)} to ${t.repo}; a pull request into ${t.base} waits for your approval`,`ok`))}}try{await this.#G()?.finish(t.session.id,{status:r,summary:n?.tally.summary??null,error:n?n.status===`succeeded`?null:n.tally.error??n.status:`the session couldn't start on this machine`,branch:n?.work?.branch??null,result:i,input_tokens:n?n.tally.inputTokens:null,output_tokens:n?n.tally.outputTokens:null,cost_micros:n?.tally.costMicros??null,model:((e.runtime===`acp`?n?.tally.model??e.model:e.model??n?.tally.model)??null)?.slice(0,100)??null}),i?.commit_count&&this.#D(e,`left ${I.bold(`${i.commit_count} commit${i.commit_count===1?``:`s`}`)} on ${I.tool(n.work.branch)} ${F(`(${i.files_changed} files)`)}`,`ok`)}catch(t){this.#D(e,`couldn't report the session's end (${t.message}); Bitfolk times it out`,`warn`)}let a=n?n.tally.limit??Pe(n.tally.error,this.#a()):null;return a&&(this.state.usage={...this.state.usage,[t.session.runtime]:{...a,at:this.#a()}},a.limited_until&&this.#D(e,`${Y(t.session.runtime)} hit its usage limit; sessions go to another tool until ${new Date(a.limited_until).toTimeString().slice(0,5)}`,`warn`)),this.#W(!0),this.#D(e,`session ended`,`idle`),$i(e),e.repo&&da(e.repo),delete this.state.agents[e.id],this.#O(),n}async#q(e,t,r,i,o={}){let s=this.#T(e),l=this.#E(e),u=Xe[e.runtime],d=e.runtime===`acp`?e.acp:void 0;if(e.runtime===`acp`&&!d)throw Error(`${e.name} has runtime acp but no ACP command`);let f=this.#a(),m;try{m=Xi(e)}catch(t){return s.etag=void 0,this.#D(e,`can't prepare its workspace: ${t.message}`,`fail`),this.#k(e,this.#A(e)),null}let h=m.branch?ea(m.dir):null,_=i?.session.retry??null,v=!!(_?.mode===`fix`&&m.created&&m.branch&&_.checkpoint_sha&&ua(m.dir,_.checkpoint_sha));v&&this.#D(e,`put back the earlier session's uncommitted work on ${I.tool(String(m.branch))}`,`ok`);let y=null;try{y=await l.runStart({client_run_id:ge(),host:de(),runtime:d?d.agent:e.runtime,reasons:t.reasons,tier:t.tier})}catch(n){if(n instanceof O&&n.status===409)return this.#D(e,`is paused by its owner; not starting`,`idle`),this.#k(e,t.next_poll_after_s*1e3||(wa?Oa:6e5)),null;this.#D(e,`couldn't record the run start (${n.message}); running anyway`,`warn`)}y||await l.status(`working`).catch(()=>{});let ee=o.fresh?null:_?.mode===`fix`?u.resumes!==!1&&_.tool_session_id?_.tool_session_id:null:u.resumes!==!1&&!m.created&&s.sessionId&&s.sessionAt&&f-s.sessionAt<432e5?s.sessionId:null,b=!!m.branch&&!!u.runnerCommits?.(e),te=ri(t,{agentName:e.name,api:e.api,branch:m.branch,runnerCommits:b,resumed:!!ee,session:i?{goal:i.session.goal?.title??null,task:i.session.task?.title??null,note:i.session.note,goalDone:i.session.goal?.done_definition??null,taskSpec:i.session.task?.spec??null,asks:i.session.asks??[],requestedBy:i.session.requested_by??`owner`,requestedByAgent:i.session.requested_by_agent??null,retry:_?{mode:_.mode,branch:_.branch,summary:_.summary,error:_.error,status:_.status,restored:v}:void 0,tool:i.session.runtime,repo:this.config.machine?ma(this.config.machine).find(t=>t.path===e.repo)?.name??null:null,computer:i.session.computer??null}:void 0}),ne=u.launch(e,te,ee);s.runStarts=[...(s.runStarts??[]).filter(e=>e>f-Ca),f],this.#O(),this.#D(e,`woke ${F(`for ${ja(t.reasons)}`)} → ${I.tool(z(d?`${Y(d.agent)} over ACP`:e.runtime))}${ee?F(`, resuming`):``}`,`start`);let re=c(k(),`logs`,e.slug);x(re,{recursive:!0,mode:448});let ie=c(re,`${new Date(f).toISOString().replace(/[:.]/g,`-`)}.jsonl`),ae=new Set([e.token]),oe=e=>[...ae].reduce((e,t)=>e.split(t).join(`[token]`),e),se=(e,t)=>g(ie,JSON.stringify({t:this.#a(),stream:e,line:oe(t)})+`
61
+ `,{mode:384}),ce=Br(),le=Fr(e),ue=e.oauth?setInterval(()=>{e.oauth.expiresAt-this.#a()>6e5||Pr(e,{now:this.#a}).then(()=>{ae.add(e.token),Fr(e)}).catch(t=>this.#D(e,`couldn't renew its sign-in mid-run: ${t.message}`,`warn`))},6e4):null,C=Le();r.tally=C,r.startedAt=this.#a();let fe={...Va(),PATH:[ce,...Ha(),...Hr()].join(a),...d?{NO_BROWSER:`1`}:{BITFOLK_TOKEN:e.token},BITFOLK_TOKEN_FILE:le,BITFOLK_API:e.api.replace(/\/+$/,``),BITFOLK_AGENT:e.name,...y?{BITFOLK_RUN_ID:y.id}:{},...i?{BITFOLK_SESSION_ID:i.session.id}:{}};d&&delete fe.BITFOLK_TOKEN;for(let[e,t]of Object.entries(fe))t&&t.length>=12&&/KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH/i.test(e)&&e!==`BITFOLK_TOKEN_FILE`&&ae.add(t);let pe=i?this.#F(i.session.id,C):null,me=i&&m.branch?this.#U(i.session.id,m,h,C):null,he=null,_e=t=>{let n=t&&R(oe(t)).trim();pe?.add(n),n&&n!==he&&(he=n,r.activity=n,r.activityAt=this.#a(),this.#D(e,n,`progress`))},ve=null,w=null,ye=!1,be,xe=null;try{be=await new Promise(t=>{let i=n(ne.bin,ne.args,{cwd:m.dir,env:fe,stdio:[`pipe`,`pipe`,`pipe`],detached:!0});if(r.child=i,i.stdin.on(`error`,()=>{}),d){let t=new It({stdin:i.stdin,stdout:i.stdout,agent:{...e,acp:d},label:Y(d.agent),cwd:m.dir,home:k(),packet:te,tally:C,clientVersion:Rt,log:se,activity:_e,kill:()=>{Na.has(i)||Pa(i)},cancelGraceMs:this.#l});r.cancel=e=>t.cancel(e),xe=t.run()}else i.stdin.end(ne.stdin),p({input:i.stdout}).on(`line`,e=>{se(`out`,e),u.parse(e,C),u.activity&&_e(u.activity(e,m.dir))});(this.#u||r.stopped)&&this.#j(r,`owner`),p({input:i.stderr}).on(`line`,e=>{se(`err`,e),e.trim()&&(w=oe(e.trim()).slice(0,300))});let a=e.timeoutMin===void 0?void 0:setTimeout(()=>{ye=!0,this.#j(r,`timeout`)||(ye=!1)},e.timeoutMin*6e4);i.on(`error`,e=>{ve=e.code===`ENOENT`?`${ne.bin} isn't installed or isn't on PATH`:e.message}),i.on(`close`,e=>{Na.add(i),clearTimeout(a),t({code:e})})})}finally{pe?.stop(),me?.stop(),ue&&clearInterval(ue),S(le,{force:!0})}let Se=xe?await xe:null;r.cancel=void 0,u.afterExit?.(ne,C),S(le,{force:!0});let Ce=this.#a()-f,T,we;if(Se?(T=r.stopped?`interrupted`:C.overBudget?`over_budget`:ve||ye||!Se.ok?`failed`:`succeeded`,we=ve??(T===`over_budget`?C.error:ye&&T===`failed`?`stopped after ${e.timeoutMin} minutes`:T===`failed`?Se.exited?w??`${Se.error??`the tool exited`}${be.code===null?``:` (exit ${be.code})`}`:Se.error??w??`the turn failed`:null),T===`failed`&&(C.error=we)):(T=r.stopped?`interrupted`:C.overBudget?`over_budget`:ve||ye||be.code!==0||C.isError?`failed`:`succeeded`,we=ve??(ye?`stopped after ${e.timeoutMin} minutes`:T===`failed`?C.error??w??`exit ${be.code}`:null)),T===`failed`&&!C.error&&(C.error=we),b&&!ve){let t=`${e.name}: ${C.summary??`work from a Bitfolk run`}`,n=`Bitfolk run${y?` ${y.id}`:``}, ${T}.`;try{e.repo&&m.branch&&Ki(m.dir,e.repo);let r=Zi(m.dir,`${t.length>72?t.slice(0,71)+`…`:t}\n\n${n}`);r&&this.#D(e,`committed its changes as ${I.bold(r)} on ${I.tool(String(m.branch))}`,`ok`)}catch(t){this.#D(e,`couldn't commit its changes: ${t.message.split(`
62
+ `)[0]}`,`warn`)}}await me?.final().catch(()=>{}),T===`failed`&&ee&&Ce<2e4?(s.sessionId=void 0,s.etag=void 0):C.sessionId&&(s.sessionId=C.sessionId,s.sessionAt=this.#a()),(T===`failed`||T===`interrupted`)&&(s.etag=void 0),this.#O();let Te={status:T,exit_code:Se?Se.ok&&T===`succeeded`?0:null:be.code,session_id:C.sessionId,input_tokens:C.inputTokens,output_tokens:C.outputTokens,cost_micros:C.costMicros,summary:C.summary,error:we};y?await l.runFinish(y.id,Te).catch(async()=>{await new Promise(e=>setTimeout(e,2e3)),await l.runFinish(y.id,Te).catch(t=>this.#D(e,`couldn't record the run end: ${t.message}`,`warn`))}):await l.status(`idle`).catch(()=>{});let Ee=this.#I();T===`succeeded`?Ee.done++:T===`interrupted`?Ee.stopped++:Ee.failed++,Ee.costMicros+=C.costMicros??0,Ee.durationMs+=Ce;let De=Ia(C),Oe=T===`succeeded`?I.ok(I.bold(T)):T===`failed`?I.err(I.bold(T)):I.warn(I.bold(T));return this.#D(e,[`${Oe} in ${Fa(Ce)}`,...De?De.split(`, `).map(e=>e.startsWith(`$`)?I.money(e):e):[]].join(` ${F(`·`)} `)+`${we?` ${I.err(`(${R(we)})`)}`:``}${C.summary?` ${F(`—`)} ${R(C.summary)}`:``}`,T===`succeeded`?`ok`:T===`failed`?`fail`:`warn`),T===`failed`?this.#k(e,this.#A(e)):(this.#n.delete(e.id),this.#k(e,Ta)),{agent:e.name,status:T,tally:C,durationMs:Ce,work:{dir:m.dir,branch:m.branch,startSha:h},resumed:!!ee}}async run(e={}){this.#_=!!e.once;let t=this.config.agents.filter(e=>!this.#T(e).disabledReason);for(let e of this.config.agents){let t=this.#T(e).disabledReason;t&&this.#D(e,`skipped: ${t}`,`idle`)}let n=this.config.machine;if(t.length===0&&!n)return this.#D(null,`nothing to run. Link this computer with \`${w} login\`.`,`warn`),[];if(e.once){let e=[];if(n){let t=await this.checkMachine();t.length===0&&!this.#p&&this.#D({name:n.name},`no sessions waiting`,`idle`),e.push(...t.map(e=>e.done))}for(let n of t){for(;this.#e.size>=this.config.maxConcurrent;)await Promise.race([...this.#e.values()].map(e=>e.done));let t=await this.check(n);t?e.push(t.done):this.#e.has(n.id)||this.#D(n,`nothing to do`,`idle`)}let r=n?setInterval(()=>void this.#N(),ka):null;try{return(await Promise.all(e)).filter(e=>e!==null)}finally{r&&clearInterval(r)}}let r=[],i=[n?`${n.name} for sessions`:null,t.length?`${t.length} agent${t.length===1?``:`s`}`:null].filter(Boolean).join(` and `);this.#D(null,`watching ${I.bold(i)} ${F(`(up to ${this.config.maxConcurrent} at once).${this.#v?``:` Ctrl-C to stop.`}`)}`,`start`);let a=this.#M(t,!!n);for(;!this.#u;){let e=this.#a();if(n&&!this.#p&&(this.#t.get(Z)??0)<=e)for(let e of await this.checkMachine())e.done.then(e=>e&&r.push(e));for(let n of t){if(this.#u||this.#e.size>=this.config.maxConcurrent)break;this.#e.has(n.id)||this.#T(n).disabledReason||(this.#t.get(n.id)??0)>e||(await this.check(n))?.done.then(e=>e&&r.push(e))}let i=Math.min(...t.filter(e=>!this.#T(e).disabledReason).map(t=>this.#t.get(t.id)??e),n&&!this.#p?this.#t.get(Z)??e:1/0),a=Math.max(1e3,Math.min(6e4,i-this.#a()));await new Promise(e=>{let t=setTimeout(e,a);this.#h=()=>{clearTimeout(t),e()}}),this.#h=null}for(let e of a)e.stop();return await Promise.all([...this.#e.values()].map(e=>e.done)),r}};const Wa=e=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e);function Ga(e){for(let t=0;t<20;t++){let t;try{t=ie(e,`utf8`).trim()}catch{return null}if(t)return Number(t)||0;Wa(25)}return 0}function Ka(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function qa(){let e=Ga(c(k(),`runner.pid`));return e&&Ka(e)?e:null}function Ja(){let e=c(k(),`runner.pid`);x(k(),{recursive:!0,mode:448});let t=String(process.pid),n=()=>{try{ie(e,`utf8`).trim()===t&&S(e,{force:!0})}catch{}};for(let r=0;r<3;r++){try{return ue(e,t,{mode:384,flag:`wx`}),n}catch(e){if(e.code!==`EEXIST`)throw e}let r=Ga(e);if(r===null)continue;if(Ka(r))throw Error(`a runner is already going (pid ${r}). Stop it first, or see \`${w} status\`.`);let i=`${e}.${process.pid}.tmp`;if(ue(i,t,{mode:384}),ce(i,e),Wa(25),Ga(e)===process.pid)return n}throw Error(`another runner is starting at the same moment. Try again, or see \`${w} status\`.`)}var Q=class extends Error{};const $=e=>e.slice(-8);function Ya(e){let t=e.trim(),n=t.split(/\r?\n/)[0].trim();if(n===t&&n.length<=200)return{title:n,note:``};let r=n;if(r.length>200){let e=r.slice(0,199),t=e.lastIndexOf(` `);r=`${(t>80?e.slice(0,t):e).trimEnd()}…`}return{title:r,note:t.slice(0,2e3)}}function Xa(e,t){if(!e)return;let n=e.trim().toLowerCase();for(let e of[n,n.replace(/-/g,`_`),Yr(n)])if(t.runtimes.includes(e))return e;throw new Q(`${e} isn't on for this computer (on: ${t.runtimes.map(e=>e.startsWith(`custom:`)?e.slice(7):e.replace(/_/g,`-`)).join(`, `)}). Turn it on with bitfolk add ${e}`)}const Za=e=>e===null?null:`$${(e/1e6).toFixed(e>0&&e<1e4?4:2)}`,Qa={queued:e=>F(`${L.idle} ${e}`),running:e=>I.sky(`${L.dot} ${e}`),succeeded:e=>I.ok(`${L.ok} ${e}`),failed:e=>I.err(`${L.fail} ${e}`),canceled:e=>F(`${L.skip} ${e}`)},$a=e=>(Qa[e]??(e=>e))(e===`succeeded`?`done`:e);function eo(e,t=Date.now()){if(!e)return``;let n=Math.max(0,Math.round((t-Date.parse(e))/1e3));return n<60?`just now`:n<3600?`${Math.floor(n/60)}m ago`:n<86400?`${Math.floor(n/3600)}h ago`:`${Math.floor(n/86400)}d ago`}function to(e,t=Date.now()){return e.started_at?ir((e.finished_at?Date.parse(e.finished_at):t)-Date.parse(e.started_at)):null}const no=e=>R(e.task?.title??e.goal?.title??`a goal`);function ro(e,t=Date.now()){if(!e.length)return[K(`${F(L.idle)} No sessions on this computer yet. Start one with ${U(`${w} run "<what to do>"`)}.`)];let n=N();return Kn(e.map(e=>{let r=Za(e.cost_micros),i=e.status===`running`?`${to(e,t)??``}`:e.status===`queued`?eo(e.created_at,t):eo(e.finished_at??e.created_at,t),a=e.status===`running`&&e.last?e.last:e.status===`failed`&&e.error?e.error:!e.task&&e.note?e.note:null,o=a?F(` · ${R(a)}`):``;return[I.tool($(e.id)),$a(e.status),F(z(Y(e.runtime))),V(`${P(no(e))}${o}`,Math.max(20,n-52)),F(z([i,r].filter(Boolean).join(` · `)))]}),{indent:2,gap:2,max:n})}function io(e,t={}){let n=t.now??Date.now(),r=[];r.push(K(`${I.tool($(e.id))} ${$a(e.status)} ${P(no(e))}`));let i=[];e.task&&e.goal&&i.push([`goal`,R(e.goal.title)]),i.push([`tool`,Y(e.runtime)+(e.model?F(` · ${R(e.model)}`):``)]),e.note&&i.push([`note`,R(e.note)]),e.retry_mode&&i.push([`retry`,`${e.retry_mode} of ${$(e.retry_of??``)}`]);let a=to(e,n);i.push([`started`,e.started_at?`${eo(e.started_at,n)}${a?F(` · ran ${a}`):``}`:F(`waiting since ${eo(e.created_at,n)}`)]);let o=(e.input_tokens??0)+(e.output_tokens??0),s=Za(e.cost_micros);(s||o)&&i.push([`spent`,[s?I.money(s):null,o?F(`${Math.round(o/1e3)}k tokens`):null].filter(Boolean).join(F(` · `))]),e.repo&&i.push([`repo`,R(e.repo)]),e.branch&&i.push([`branch`,I.tool(e.branch)]);let c=e.result?.pull_request;if(c?.url){let e=c.state===`merged`?I.ok(`merged`):c.state===`failed`?I.err(`merge failed: ${R(c.error??``)}`):F(`waits for your approval to merge`);i.push([`pull request`,`${R(c.url)} ${e}`])}else e.result?.push?i.push([`pull request`,F(`pushed to ${R(e.result.push.repo)}; opening one needs GitHub connected in Bitfolk`)]):e.result?.push_error&&i.push([`pull request`,I.warn(`not pushed: ${R(e.result.push_error)}`)]);r.push(...Jn(i,2));let l=e.progress??[];if(l.length){r.push(``);for(let e of l)r.push(K(` ${F(ao(e.at))} ${R(e.text)}`,12))}let u=e.result?.commits??[];if(u.length){r.push(``,` ${P(`${e.result.commit_count} commit${e.result.commit_count===1?``:`s`}`)}${F(`, ${e.result.files_changed} files, +${e.result.insertions} -${e.result.deletions}`)}`);for(let e of u.slice(0,10))r.push(K(` ${I.tool(e.sha.slice(0,7))} ${R(e.subject)}`,10))}return e.summary&&r.push(``,K(W(R(e.summary)))),e.error&&e.status!==`succeeded`&&r.push(``,K(`${I.err(L.fail)} ${R(e.error)}`)),t.localLog&&r.push(``,K(F(`The tool's full output on this computer: ${t.localLog}`))),r}const ao=e=>new Date(e).toTimeString().slice(0,8);function oo(e){return e.length?Kn(e.map(e=>[I.tool($(e.id)),P(R(e.name??e.title)),F(z(e.tasks?`${e.open_tasks} of ${e.tasks} tasks open`:`no tasks yet`)),e.status===`paused`?I.warn(`paused`):``]),{indent:2,gap:2,max:N()}):[K(`${F(L.idle)} No open goals. ${U(`${w} run "<what to do>"`)} starts one.`)]}function so(e){let t=c(k(),`logs`,`builder-${$(e)}`);return b(t)?t:null}function co(e){let t=e.trim().toLowerCase();if(!/^[0-9a-f-]{4,36}$/.test(t))throw new Q(`"${e}" isn't a session id. Give one, or its last 4 or more characters, as ${w} sessions lists them`);return t}function lo(e){if(!e)throw new Q(`link this computer first: ${w} login`);return{m:e,api:new Wt(e.api,()=>Pr(e))}}function uo(e){throw e instanceof O&&[400,404,429].includes(e.status)&&e.message!==`invalid_body`?new Q(e.message):e}async function fo(e,t,n,r){let{m:i,api:a}=lo(e),o=t.join(` `).trim();if(!o&&!n.goal)throw new Q(`say what to do: ${w} run "fix the flaky login test", or name a goal with --goal`);if(n.task&&!n.goal)throw new Q(`--task goes with --goal: ${w} run --goal <goal> --task <task>`);let s=Xa(n.runtime,i)??null,c=n.in?ga(i,n.in).name:null,l=n.goal?{goal:n.goal,task:n.task??null,note:o.slice(0,2e3)}:{...Ya(o),goal:null},u=await q(n.goal?`Starting a session`:`Starting a goal`,()=>a.start({...l,runtime:s,repo:c})).catch(uo),d=u.session,f=R(d.goal?.title??``);if(console.log(K(W(`${u.goal_created?`New goal`:`Goal`} ${P(f)} ${F(`(${$(d.goal?.id??``)})`)}`))),console.log(K(`${I.ok(L.ok)} Session ${I.tool($(d.id))} ${F(`on`)} ${Y(d.runtime)}${d.repo?F(` in ${R(d.repo)}`):``}${d.task?F(` for ${R(d.task.title)}`):``}${u.ahead?F(` · ${u.ahead} waiting before it`):``}`)),n.detach)console.log(K(F(r.upRunning?`${w} up takes it here. Follow it with ${w} logs ${$(d.id)} --follow.`:`It waits until ${w} up runs on this computer.`)));else if(r.upRunning)console.log(K(F(`${w} up on this computer takes it. Following along; Ctrl-C stops following, not the session.`))+`
63
63
  `),await po(a,d.id);else{u.ahead&&console.log(K(G(`${u.ahead} older session${u.ahead===1?``:`s`} waiting here run${u.ahead===1?`s`:``} first.`))),console.log(K(F(`Running it here, since ${w} up isn't. Ctrl-C stops it.`))+`
64
64
  `),await r.runHere();let e=await a.session(d.id).catch(()=>null);e&&console.log(`
65
65
  `+io(e,{localLog:so(e.id)}).join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfolk",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "The CLI for Bitfolk, AI agent orchestration for the agents you already use. Run Claude Code, Codex, OpenCode, Gemini CLI and more on your own computer, from any device.",
5
5
  "keywords": [
6
6
  "bitfolk",