@codweft/runner
Advanced tools
+4
-4
| #!/usr/bin/env node | ||
| import{spawn as M}from"child_process";import{realpathSync as N}from"fs";import{appendFile as J,mkdir as R,mkdtemp as z,readFile as T,rm as B,writeFile as U}from"fs/promises";import{tmpdir as L}from"os";import{dirname as W,join as l}from"path";import{fileURLToPath as G}from"url";import{z as n}from"zod";var A=n.union([n.literal("debug"),n.literal("info"),n.literal("warn"),n.literal("error")]),I=n.object({job:n.object({jobId:n.string(),commandId:n.string(),owner:n.string(),repo:n.string(),fullName:n.string(),defaultBranch:n.string(),command:n.string(),instruction:n.string(),status:n.union([n.literal("queued"),n.literal("in_progress"),n.literal("success"),n.literal("failure"),n.literal("cancelled"),n.literal("timed_out")]).optional(),workItemNumber:n.number().optional(),changeRequestNumber:n.number().optional()}),checkout:n.object({checkoutMode:n.union([n.literal("default_branch"),n.literal("change_request_head")]),checkoutRef:n.string().min(1),checkoutSha:n.string().min(1).optional(),checkoutFetchRef:n.string().min(1).optional(),baseRef:n.string().min(1).optional(),publishBaseRef:n.string().min(1)}),modelRoutingPlan:n.object({retryGroup:n.string(),routes:n.array(n.object({provider:n.string(),model:n.string(),secretName:n.string()}))}),mcp:n.object({endpoint:n.string().url()})}),O=16384,V=20,Z=1e3,$=[":(exclude).codweft/result.json",":(exclude).mcp.json"],K=[".codweft/result.json",".mcp.json"],X=["PATH","HOME","TMPDIR","USER","LANG","LC_ALL","CI"],Y=new Set(["failure","cancelled","timed_out"]),Q=/(token|secret|password|api[_-]?key|private[_-]?key|bearer)/i;function S(e){let t=process.env[e];if(!t)throw new Error(`${e} is required`);return t}function v(e,t={}){let r=e.replace(/https:\/\/([^:\s/@]+):([^@\s]+)@github\.com/gi,"https://$1:[redacted]@github.com").replace(/authorization:\s*basic\s+[A-Za-z0-9+/=._~-]+/gi,"AUTHORIZATION: basic [redacted]").replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi,"Bearer [redacted]").replace(/(CODWEFT_(?:BOOTSTRAP|MCP)_TOKEN=)[^\s]+/g,"$1[redacted]");for(let[o,s]of Object.entries({...process.env,...t}))!s||s.length<8||!Q.test(o)||(r=r.split(s).join(`[redacted:${o}]`));return r}function ee(e){return Buffer.byteLength(e)<=O?e:`${e.slice(0,O-128)} | ||
| [truncated by codweft-runner]`}async function w(e,t){let r=S("CODWEFT_API_URL").replace(/\/$/,""),o=S("CODWEFT_BOOTSTRAP_TOKEN"),s=await fetch(`${r}${e}`,{...t,headers:{authorization:`Bearer ${o}`,"content-type":"application/json",...t?.headers}});if(!s.ok)throw new Error(`${e} failed: ${s.status} ${await s.text()}`);return s}var j=class{constructor(t,r){this.jobId=t;this.logPath=r}jobId;logPath;sequence=0;buffer=[];flushTimer=null;async event(t,r=!1){let o={...t,sequence:t.sequence??++this.sequence,message:ee(v(t.message)),redacted:!0};if(this.logPath&&await C(this.logPath,`${JSON.stringify({...o,createdAt:new Date().toISOString()})} | ||
| `).catch(()=>{}),this.buffer.push(o),r||this.buffer.length>=V||t.severity==="error"){await this.flush();return}this.scheduleFlush()}async timeline(t,r="info",o){await this.event({type:"runner",source:"runner",channel:"timeline",message:t,severity:r,step:o,visibility:r==="error"?"user":"debug",redacted:!0},r==="error")}async flush(){this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null);let t=this.buffer.splice(0,this.buffer.length);t.length!==0&&await w(`/api/runner/jobs/${this.jobId}/events`,{method:"POST",body:JSON.stringify({events:t})}).catch(()=>{})}scheduleFlush(){this.flushTimer||(this.flushTimer=setTimeout(()=>{this.flush().catch(()=>{})},Z))}};async function te(e){let t=await w(`/api/runner/jobs/${e}/forge-token`,{method:"POST"});return n.object({token:n.string(),username:n.string()}).parse(await t.json())}function re({fullName:e,username:t,token:r}){let o=`https://github.com/${e}.git`,s=Buffer.from(`${t}:${r}`).toString("base64");return{repoUrl:o,extraHeaderConfig:`http.https://github.com/.extraheader=AUTHORIZATION: basic ${s}`}}function ne(e,t){let r=["fetch","origin",e,"--depth=1"];return t?["-c",t,...r]:r}function se(e,t){return["-c",t,"-c","core.hooksPath=/dev/null","push","origin",`HEAD:refs/heads/${e}`,"--force-with-lease"]}function oe(){return["add","-A","--",".",...$]}function ie(e){return["-c","core.hooksPath=/dev/null","commit","-m",e]}function ae(e){return["diff","--name-only",`${e}..HEAD`,"--",...K]}function D(e){return`codweft/${e.replace(/[^A-Za-z0-9._-]/g,"-")}`}function ce({jobId:e,baseRef:t,title:r,body:o,draft:s}){return{title:r,body:o,draft:s,head:D(e),base:t}}function ue(e){return/\b(create|open|submit|raise)\b[\s\S]{0,80}\b(pr|pull request|change request)\b/i.test(e)}function le(e,t){return["fix","implement","resolve_conflicts"].includes(e)?!0:e==="task"&&ue(t)}function x(e=process.env){let t={};for(let r of X){let o=e[r];o&&(t[r]=o)}return t.HOME&&(t.PATH=`${t.HOME}/.pi/bin${t.PATH?`:${t.PATH}`:""}`),t}function de({mcpToken:e,secretName:t,secretValue:r,sourceEnv:o=process.env}){let s=x(o);if(!r)throw new Error(`${t} is not configured`);return s[t]=r,s.CODWEFT_MCP_TOKEN=e,s}var ge=n.object({changeRequest:n.object({title:n.string().min(1).max(256),body:n.string().max(65536).optional(),draft:n.boolean().optional()}).optional()});function pe(e){return ge.parse(JSON.parse(e))}async function he(e){let t=await w(`/api/runner/jobs/${e}/mcp-token`,{method:"POST"});return n.object({mcpToken:n.string()}).parse(await t.json()).mcpToken}async function fe(e,t){let r=await w(`/api/runner/jobs/${e}/provider-secret`,{method:"POST",body:JSON.stringify({secretName:t})});return n.object({configured:n.boolean(),value:n.string().optional()}).parse(await r.json())}async function C(e,t){await R(W(e),{recursive:!0}),await J(e,t)}async function g(e,t,r){let o=r.source??"runner",s=v(`${e} ${t.join(" ")}`,r.env);await r.context.reporter.timeline(`running ${s}`,"info",r.step);let i=l(r.context.logsDir,`${r.logPrefix??r.step}-stdout.log`),a=l(r.context.logsDir,`${r.logPrefix??r.step}-stderr.log`),c="",u="",d=[];return new Promise((m,b)=>{async function p(){await Promise.allSettled(d)}let F=r.inheritEnv===!1||e==="git"&&r.inheritEnv!==!0?{...x(),...r.env}:{...process.env,...r.env},k=M(e,t,{cwd:r.cwd,env:F,stdio:["ignore","pipe","pipe"]});k.stdout?.on("data",y=>{let h=v(y.toString("utf8"),r.env);c+=h,process.stdout.write(h),d.push(Promise.all([C(i,h),q(r.context,{text:h,source:o,channel:"stdout",step:r.step,normalizeEvent:r.normalizeEvent})]))}),k.stderr?.on("data",y=>{let h=v(y.toString("utf8"),r.env);u+=h,process.stderr.write(h),d.push(Promise.all([C(a,h),q(r.context,{text:h,source:o,channel:"stderr",step:r.step,normalizeEvent:r.normalizeEvent})]))}),k.on("exit",y=>{let h=y??1;p().then(()=>{h===0?m({stdout:c,stderr:u,exitCode:h}):b(new Error(`${e} exited with ${h}`))}).catch(b)}),k.on("error",y=>{p().then(()=>b(y)).catch(b)})})}async function q(e,{text:t,source:r,channel:o,step:s,normalizeEvent:i}){let a=t.split(/\r?\n/).filter(c=>c.trim().length>0);for(let c of a.slice(-10)){let u=i?.(c,o);u!==!1&&await e.reporter.event(u??{type:"process_output",source:r,channel:o,message:c,severity:o==="stderr"?"warn":"debug",step:s,visibility:"debug",redacted:!0})}}function me(e){let t=e.job.changeRequestNumber!==void 0?`change_request #${e.job.changeRequestNumber}`:e.job.workItemNumber!==void 0?`work_item #${e.job.workItemNumber}`:"repository";return["You are Codweft's coding agent running inside a portable runner.","Use local filesystem, shell, and git for repository work.","Use the Codweft MCP server for all forge operations: comments, reviews, labels, checks, and change requests.","Do not call forge APIs directly when an MCP tool exists.","Do not push branches or commits. The runner owns publishing.",'When code changes should be published, leave the changes locally and write .codweft/result.json with {"changeRequest":{"title":"...","body":"...","draft":false}}.',"Do not call create_change_request for local code changes; the runner will push the branch first and then create it.",`Command: ${e.job.command}`,`Target: ${t}`,`Instruction: ${e.job.instruction}`,"","For free-form task commands, default to diagnosis or a plan unless the instruction explicitly asks for code changes or a change request.","Always report progress and final success or failure through Codweft MCP."].join(` | ||
| `)}async function be(e,t){let r={mcpServers:{codweft:{url:t,auth:"bearer",bearerTokenEnv:"CODWEFT_MCP_TOKEN",lifecycle:"keep-alive",directTools:!0}}};await U(l(e,".mcp.json"),JSON.stringify(r,null,2))}function we(e,t){let r=e.trim();if(!r.startsWith("{")||!r.endsWith("}"))return null;let o;try{o=JSON.parse(r)}catch{return null}let s=n.object({type:n.string().optional(),message:n.string().optional(),content:n.string().optional(),toolCallId:n.string().optional(),toolName:n.string().optional(),level:A.optional(),severity:A.optional(),role:n.string().optional()}).passthrough().safeParse(o);if(!s.success)return null;if(s.data.type==="tool_execution_start")return{type:"tool_execution_start",source:"agent",channel:"tool_call",message:s.data.toolName?`starting tool ${s.data.toolName}`:"starting tool call",severity:"info",step:"agent_run",visibility:"debug",redacted:!0,metadata:{toolCallId:s.data.toolCallId,toolName:s.data.toolName}};if(s.data.type==="tool_execution_end")return{type:"tool_execution_end",source:"agent",channel:"tool_call",message:s.data.toolName?`completed tool ${s.data.toolName}`:"completed tool call",severity:"info",step:"agent_run",visibility:"debug",redacted:!0,metadata:{toolCallId:s.data.toolCallId,toolName:s.data.toolName}};if(s.data.type==="session"||s.data.type==="agent_start"||s.data.type==="turn_start"||s.data.type==="turn_end"||s.data.type==="message_start"||s.data.type==="message_update"||s.data.type==="message_end")return!1;let i=s.data.message??s.data.content;return i?{type:s.data.type??"agent_message",source:"agent",channel:s.data.role==="tool"?"tool_call":"agent_message",message:i,severity:s.data.severity??s.data.level??(t==="stderr"?"warn":"info"),visibility:"user",redacted:!0,metadata:{rawType:s.data.type}}:null}var E={async install(e){let t=x();await g("sh",["-lc","command -v pi >/dev/null 2>&1 || curl -fsSL https://pi.dev/install.sh | sh"],{cwd:e.workspace,env:t,inheritEnv:!1,step:"agent_install",logPrefix:"pi-install",context:e}),await g("pi",["install","npm:pi-mcp-adapter@2.5.4"],{cwd:e.workspace,env:t,inheritEnv:!1,step:"agent_install",logPrefix:"pi-install",context:e})},async configureMcp(e,t){await be(e.workspace,t)},async run(e,t,r){let o=me(t),s=null;for(let i of t.modelRoutingPlan.routes)try{let a=await fe(t.job.jobId,i.secretName);if(!a.configured||!a.value){await e.reporter.event({type:"model_route_skipped",source:"model_router",channel:"timeline",message:`skipping ${i.provider}/${i.model}: ${i.secretName} is not configured`,severity:"warn",step:"model_route",visibility:"debug",redacted:!0});continue}await e.reporter.event({type:"agent_started",source:"agent",channel:"timeline",message:`starting Pi with ${i.provider}/${i.model}`,severity:"info",step:"agent_run",visibility:"user",redacted:!0},!0),await g("pi",["--mode","json","--provider",i.provider,"--model",i.model,o],{cwd:e.workspace,env:de({mcpToken:r,secretName:i.secretName,secretValue:a.value}),inheritEnv:!1,step:"agent_run",source:"agent",logPrefix:"agent",normalizeEvent:this.normalizeEvent,context:e});return}catch(a){s=a instanceof Error?a:new Error(String(a)),await e.reporter.event({type:"model_route_failed",source:"model_router",channel:"timeline",message:`model route failed: ${s.message}`,severity:"warn",step:"model_route",visibility:"user",redacted:!0},!0)}throw s??new Error("No configured model routes were available")},normalizeEvent:we,async collectArtifacts(e){await f(e.jobId,"log","agent-stdout.log",l(e.logsDir,"agent-stdout.log")),await f(e.jobId,"log","agent-stderr.log",l(e.logsDir,"agent-stderr.log"))}};async function H(e,t,r,o,s="text/plain"){let i=await w(`/api/runner/jobs/${e}/artifacts`,{method:"POST",body:JSON.stringify({kind:t,fileName:r,name:r,contentType:s,size:Buffer.byteLength(o)})}),a=n.object({uploadUrl:n.string(),headers:n.record(n.string(),n.string())}).parse(await i.json());await fetch(a.uploadUrl,{method:"PUT",headers:a.headers,body:o})}async function f(e,t,r,o){let s=await T(o,"utf8").catch(()=>"");s&&await H(e,t,r,s).catch(()=>{})}var _=class extends Error{constructor(r){super(`Job was already marked ${r} by MCP`);this.status=r}status};function ye(e){return!!(e&&Y.has(e))}async function ve(e){let t=await w(`/api/runner/jobs/${e}`),r=I.parse(await t.json());if(ye(r.job.status))throw new _(r.job.status)}async function ke(e){let t=l(e,".codweft","result.json"),r=await T(t,"utf8").catch(s=>{if(s.code==="ENOENT")return null;throw s});if(r===null)return null;let o=pe(r);return await B(t,{force:!0}),o}async function P(e,t,r,o=r){return(await g("git",t,{cwd:e.workspace,step:r,logPrefix:o,context:e})).stdout.trim()}async function Ee(e){return(await P(e,["status","--porcelain","--",".",...$],"publish","git-publish")).length>0}async function _e(e,t){return(await P(e,["diff","--name-only",`${t}..HEAD`,"--",".",...$],"publish","git-publish")).length>0}async function Pe(e,t){let r=await P(e,ae(t),"publish","git-publish");if(r)throw new Error(`Agent committed runner control files, refusing to publish: ${r.split(/\r?\n/).join(", ")}`)}async function Re(e,t){await Ee(t)&&(await g("git",["config","user.name","codweft[bot]"],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",["config","user.email","codweft[bot]@users.noreply.github.com"],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",oe(),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",ie(`codweft: apply ${e.job.command} for ${e.job.jobId}`),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}))}async function Se(e,t,r,o){let s=await fetch(e,{method:"POST",headers:{authorization:`Bearer ${t}`,"content-type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`runner-${Date.now()}`,method:"tools/call",params:{name:r,arguments:o}})});if(!s.ok)throw new Error(`MCP ${r} failed: ${s.status} ${await s.text()}`);let i=n.object({error:n.object({message:n.string()}).optional(),result:n.object({content:n.array(n.object({type:n.string(),text:n.string()})).optional()}).optional()}).parse(await s.json());if(i.error)throw new Error(`MCP ${r} failed: ${i.error.message}`);let a=i.result?.content?.[0]?.text;return a?JSON.parse(a):{}}async function je({payload:e,context:t,baseSha:r,gitExtraHeaderConfig:o,mcpToken:s}){let i=await ke(t.workspace),a=le(e.job.command,e.job.instruction),c=!!i?.changeRequest;if(c&&!a)throw new Error(`${e.job.command} jobs are not allowed to publish change requests`);let u=D(e.job.jobId);await g("git",["checkout","-B",u],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await Re(e,t),await Pe(t,r);let d=await _e(t,r);if(!c){if(d)throw new Error("Agent made code changes but did not write .codweft/result.json with change request metadata");return null}if(!d)throw new Error("Agent requested a change request but produced no publishable code changes");await t.reporter.event({type:"publish_started",source:"runner",channel:"timeline",message:`publishing ${u}`,severity:"info",step:"publish",visibility:"user",redacted:!0},!0),await g("git",se(u,o),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t});let m=i?.changeRequest;if(!m)return null;let b=await Se(e.mcp.endpoint,s,"create_change_request",ce({jobId:e.job.jobId,baseRef:e.checkout.publishBaseRef,title:m.title,body:m.body,draft:m.draft})),p=n.object({number:n.number().optional(),html_url:n.string().optional(),url:n.string().optional()}).passthrough().parse(b);return await t.reporter.event({type:"change_request_created",source:"runner",channel:"result",message:p.html_url?`created change request ${p.html_url}`:`created change request${p.number?` #${p.number}`:""}`,severity:"info",step:"publish",visibility:"user",redacted:!0,metadata:{number:p.number,url:p.html_url??p.url}},!0),{changeRequest:{number:p.number,url:p.html_url??p.url,branch:u}}}async function Ce(){let e=S("CODWEFT_JOB_ID"),t=await w(`/api/runner/jobs/${e}`),r=I.parse(await t.json()),o=await z(l(L(),"codweft-")),s=l(o,"repo"),i=l(o,"logs");await R(s,{recursive:!0}),await R(i,{recursive:!0});let a=new j(e,l(i,"runner.log")),c={jobId:e,workspace:s,logsDir:i,reporter:a};try{await a.event({type:"runner_started",source:"runner",channel:"timeline",message:"runner started",severity:"info",step:"bootstrap",visibility:"user",redacted:!0},!0);let u=await te(e),d=re({fullName:r.job.fullName,username:u.username,token:u.token});await g("git",["-c",d.extraHeaderConfig,"clone","--recurse-submodules",d.repoUrl,s],{step:"clone",logPrefix:"git-clone",context:c}),await g("git",["remote","set-url","origin",d.repoUrl],{cwd:s,step:"clone",logPrefix:"git-clone",context:c}),await Te(r,c,d.extraHeaderConfig);let m=await P(c,["rev-parse","HEAD"],"checkout","git-checkout"),b=await he(e);await E.install(c),await E.configureMcp(c,r.mcp.endpoint),await E.run(c,r,b),await ve(e);let p=await je({payload:r,context:c,baseSha:m,gitExtraHeaderConfig:d.extraHeaderConfig,mcpToken:b});await w(`/api/runner/jobs/${e}/result`,{method:"POST",body:JSON.stringify({status:"success",result:p??void 0})}),await a.event({type:"job_completed",source:"runner",channel:"result",message:"job completed successfully",severity:"info",step:"result",visibility:"user",redacted:!0},!0)}catch(u){let d=u instanceof Error?u.stack??u.message:String(u),m=u instanceof _?u.status:"failure";await a.event({type:"job_failed",source:"runner",channel:"result",message:d,severity:"error",step:"result",visibility:"user",redacted:!0},!0),await H(e,"log","runner-error.txt",v(d)).catch(()=>{}),await w(`/api/runner/jobs/${e}/result`,{method:"POST",body:JSON.stringify({status:m,failureReason:v(d).slice(0,16e3)})}).catch(()=>{}),process.exitCode=1}finally{await a.flush(),await f(e,"log","git-clone-stdout.log",l(i,"git-clone-stdout.log")),await f(e,"log","git-clone-stderr.log",l(i,"git-clone-stderr.log")),await f(e,"log","git-checkout-stdout.log",l(i,"git-checkout-stdout.log")),await f(e,"log","git-checkout-stderr.log",l(i,"git-checkout-stderr.log")),await f(e,"log","git-publish-stdout.log",l(i,"git-publish-stdout.log")),await f(e,"log","git-publish-stderr.log",l(i,"git-publish-stderr.log")),await f(e,"log","pi-install-stdout.log",l(i,"pi-install-stdout.log")),await f(e,"log","pi-install-stderr.log",l(i,"pi-install-stderr.log")),await f(e,"log","runner.log",l(i,"runner.log")),await E.collectArtifacts(c),await T(l(s,".mcp.json"),"utf8").catch(()=>"")}}async function Te(e,t,r){if(e.checkout.checkoutMode==="change_request_head"){if(!e.checkout.checkoutSha)throw new Error("Change request checkout is missing checkoutSha");if(!e.checkout.checkoutFetchRef)throw new Error("Change request checkout is missing checkoutFetchRef");await t.reporter.event({type:"checkout_prepared",source:"runner",channel:"timeline",message:`checking out change_request head ${e.checkout.checkoutSha}`,severity:"info",step:"checkout",visibility:"user",redacted:!0},!0),await g("git",ne(e.checkout.checkoutFetchRef,r),{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t}),await g("git",["checkout","--detach","FETCH_HEAD"],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t});let s=(await g("git",["rev-parse","HEAD"],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t})).stdout.trim();if(s!==e.checkout.checkoutSha)throw new Error(`Checkout SHA mismatch: expected ${e.checkout.checkoutSha}, got ${s}`);return}await t.reporter.event({type:"checkout_prepared",source:"runner",channel:"timeline",message:`checking out default branch ${e.checkout.checkoutRef}`,severity:"info",step:"checkout",visibility:"user",redacted:!0},!0),await g("git",["checkout",e.checkout.checkoutRef],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t})}function $e(e=process.argv[1],t=import.meta.url){let r=G(t);if(!e)return!1;try{return N(e)===N(r)}catch{return e===r}}$e()&&Ce().catch(e=>{console.error(v(e instanceof Error?e.stack??e.message:String(e))),process.exit(1)});export{Pe as assertNoCommittedControlFiles,de as buildAgentEnv,ne as buildChangeRequestFetchArgs,ae as buildCommittedControlFilesDiffArgs,ce as buildCreateChangeRequestArgs,oe as buildGitAddPublishArgs,ie as buildGitCommitArgs,re as buildGitHubCloneAuth,se as buildGitPushArgs,D as buildPublishBranchName,x as buildSafeProcessEnv,le as canPublishForCommand,$e as isCliEntrypoint,we as normalizePiEvent,pe as parseRunnerResultJson,v as redactText,g as runProcess,ue as taskExplicitlyRequestsChangeRequest}; | ||
| import{spawn as M}from"child_process";import{realpathSync as N}from"fs";import{appendFile as J,mkdir as R,mkdtemp as z,readFile as T,rm as B,writeFile as U}from"fs/promises";import{tmpdir as L}from"os";import{dirname as W,join as l}from"path";import{fileURLToPath as G}from"url";import{z as n}from"zod";var A=n.union([n.literal("debug"),n.literal("info"),n.literal("warn"),n.literal("error")]),I=n.object({job:n.object({jobId:n.string(),commandId:n.string(),owner:n.string(),repo:n.string(),fullName:n.string(),defaultBranch:n.string(),command:n.string(),instruction:n.string(),status:n.union([n.literal("queued"),n.literal("in_progress"),n.literal("success"),n.literal("failure"),n.literal("cancelled"),n.literal("timed_out")]).optional(),workItemNumber:n.number().optional(),changeRequestNumber:n.number().optional()}),checkout:n.object({checkoutMode:n.union([n.literal("default_branch"),n.literal("change_request_head")]),checkoutRef:n.string().min(1),checkoutSha:n.string().min(1).optional(),checkoutFetchRef:n.string().min(1).optional(),baseRef:n.string().min(1).optional(),publishBaseRef:n.string().min(1)}),modelRoutingPlan:n.object({retryGroup:n.string(),routes:n.array(n.object({provider:n.string(),model:n.string(),credentialId:n.string(),credentialName:n.string().optional(),secretName:n.string()}))}),mcp:n.object({endpoint:n.string().url()})}),O=16384,V=20,Z=1e3,$=[":(exclude).codweft/result.json",":(exclude).mcp.json"],K=[".codweft/result.json",".mcp.json"],X=["PATH","HOME","TMPDIR","USER","LANG","LC_ALL","CI"],Y=new Set(["failure","cancelled","timed_out"]),Q=/(token|secret|password|api[_-]?key|private[_-]?key|bearer)/i;function S(e){let t=process.env[e];if(!t)throw new Error(`${e} is required`);return t}function v(e,t={}){let r=e.replace(/https:\/\/([^:\s/@]+):([^@\s]+)@github\.com/gi,"https://$1:[redacted]@github.com").replace(/authorization:\s*basic\s+[A-Za-z0-9+/=._~-]+/gi,"AUTHORIZATION: basic [redacted]").replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi,"Bearer [redacted]").replace(/(CODWEFT_(?:BOOTSTRAP|MCP)_TOKEN=)[^\s]+/g,"$1[redacted]");for(let[i,s]of Object.entries({...process.env,...t}))!s||s.length<8||!Q.test(i)||(r=r.split(s).join(`[redacted:${i}]`));return r}function ee(e){return Buffer.byteLength(e)<=O?e:`${e.slice(0,O-128)} | ||
| [truncated by codweft-runner]`}async function w(e,t){let r=S("CODWEFT_API_URL").replace(/\/$/,""),i=S("CODWEFT_BOOTSTRAP_TOKEN"),s=await fetch(`${r}${e}`,{...t,headers:{authorization:`Bearer ${i}`,"content-type":"application/json",...t?.headers}});if(!s.ok)throw new Error(`${e} failed: ${s.status} ${await s.text()}`);return s}var j=class{constructor(t,r){this.jobId=t;this.logPath=r}jobId;logPath;sequence=0;buffer=[];flushTimer=null;async event(t,r=!1){let i={...t,sequence:t.sequence??++this.sequence,message:ee(v(t.message)),redacted:!0};if(this.logPath&&await C(this.logPath,`${JSON.stringify({...i,createdAt:new Date().toISOString()})} | ||
| `).catch(()=>{}),this.buffer.push(i),r||this.buffer.length>=V||t.severity==="error"){await this.flush();return}this.scheduleFlush()}async timeline(t,r="info",i){await this.event({type:"runner",source:"runner",channel:"timeline",message:t,severity:r,step:i,visibility:r==="error"?"user":"debug",redacted:!0},r==="error")}async flush(){this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null);let t=this.buffer.splice(0,this.buffer.length);t.length!==0&&await w(`/api/runner/jobs/${this.jobId}/events`,{method:"POST",body:JSON.stringify({events:t})}).catch(()=>{})}scheduleFlush(){this.flushTimer||(this.flushTimer=setTimeout(()=>{this.flush().catch(()=>{})},Z))}};async function te(e){let t=await w(`/api/runner/jobs/${e}/forge-token`,{method:"POST"});return n.object({token:n.string(),username:n.string()}).parse(await t.json())}function re({fullName:e,username:t,token:r}){let i=`https://github.com/${e}.git`,s=Buffer.from(`${t}:${r}`).toString("base64");return{repoUrl:i,extraHeaderConfig:`http.https://github.com/.extraheader=AUTHORIZATION: basic ${s}`}}function ne(e,t){let r=["fetch","origin",e,"--depth=1"];return t?["-c",t,...r]:r}function se(e,t){return["-c",t,"-c","core.hooksPath=/dev/null","push","origin",`HEAD:refs/heads/${e}`,"--force-with-lease"]}function ie(){return["add","-A","--",".",...$]}function oe(e){return["-c","core.hooksPath=/dev/null","commit","-m",e]}function ae(e){return["diff","--name-only",`${e}..HEAD`,"--",...K]}function D(e){return`codweft/${e.replace(/[^A-Za-z0-9._-]/g,"-")}`}function ce({jobId:e,baseRef:t,title:r,body:i,draft:s}){return{title:r,body:i,draft:s,head:D(e),base:t}}function ue(e){return/\b(create|open|submit|raise)\b[\s\S]{0,80}\b(pr|pull request|change request)\b/i.test(e)}function le(e,t){return["fix","implement","resolve_conflicts"].includes(e)?!0:e==="task"&&ue(t)}function x(e=process.env){let t={};for(let r of X){let i=e[r];i&&(t[r]=i)}return t.HOME&&(t.PATH=`${t.HOME}/.pi/bin${t.PATH?`:${t.PATH}`:""}`),t}function de({mcpToken:e,secretName:t,secretValue:r,sourceEnv:i=process.env}){let s=x(i);if(!r)throw new Error(`${t} is not configured`);return s[t]=r,s.CODWEFT_MCP_TOKEN=e,s}var ge=n.object({changeRequest:n.object({title:n.string().min(1).max(256),body:n.string().max(65536).optional(),draft:n.boolean().optional()}).optional()});function pe(e){return ge.parse(JSON.parse(e))}async function he(e){let t=await w(`/api/runner/jobs/${e}/mcp-token`,{method:"POST"});return n.object({mcpToken:n.string()}).parse(await t.json()).mcpToken}async function fe(e,t){let r=await w(`/api/runner/jobs/${e}/provider-secret`,{method:"POST",body:JSON.stringify({credentialId:t})});return n.object({configured:n.boolean(),credentialId:n.string().optional(),credentialName:n.string().optional(),secretName:n.string().optional(),value:n.string().optional()}).parse(await r.json())}async function C(e,t){await R(W(e),{recursive:!0}),await J(e,t)}async function g(e,t,r){let i=r.source??"runner",s=v(`${e} ${t.join(" ")}`,r.env);await r.context.reporter.timeline(`running ${s}`,"info",r.step);let o=l(r.context.logsDir,`${r.logPrefix??r.step}-stdout.log`),a=l(r.context.logsDir,`${r.logPrefix??r.step}-stderr.log`),c="",u="",d=[];return new Promise((m,b)=>{async function p(){await Promise.allSettled(d)}let F=r.inheritEnv===!1||e==="git"&&r.inheritEnv!==!0?{...x(),...r.env}:{...process.env,...r.env},k=M(e,t,{cwd:r.cwd,env:F,stdio:["ignore","pipe","pipe"]});k.stdout?.on("data",y=>{let h=v(y.toString("utf8"),r.env);c+=h,process.stdout.write(h),d.push(Promise.all([C(o,h),q(r.context,{text:h,source:i,channel:"stdout",step:r.step,normalizeEvent:r.normalizeEvent})]))}),k.stderr?.on("data",y=>{let h=v(y.toString("utf8"),r.env);u+=h,process.stderr.write(h),d.push(Promise.all([C(a,h),q(r.context,{text:h,source:i,channel:"stderr",step:r.step,normalizeEvent:r.normalizeEvent})]))}),k.on("exit",y=>{let h=y??1;p().then(()=>{h===0?m({stdout:c,stderr:u,exitCode:h}):b(new Error(`${e} exited with ${h}`))}).catch(b)}),k.on("error",y=>{p().then(()=>b(y)).catch(b)})})}async function q(e,{text:t,source:r,channel:i,step:s,normalizeEvent:o}){let a=t.split(/\r?\n/).filter(c=>c.trim().length>0);for(let c of a.slice(-10)){let u=o?.(c,i);u!==!1&&await e.reporter.event(u??{type:"process_output",source:r,channel:i,message:c,severity:i==="stderr"?"warn":"debug",step:s,visibility:"debug",redacted:!0})}}function me(e){let t=e.job.changeRequestNumber!==void 0?`change_request #${e.job.changeRequestNumber}`:e.job.workItemNumber!==void 0?`work_item #${e.job.workItemNumber}`:"repository";return["You are Codweft's coding agent running inside a portable runner.","Use local filesystem, shell, and git for repository work.","Use the Codweft MCP server for all forge operations: comments, reviews, labels, checks, and change requests.","Do not call forge APIs directly when an MCP tool exists.","Do not push branches or commits. The runner owns publishing.",'When code changes should be published, leave the changes locally and write .codweft/result.json with {"changeRequest":{"title":"...","body":"...","draft":false}}.',"Do not call create_change_request for local code changes; the runner will push the branch first and then create it.",`Command: ${e.job.command}`,`Target: ${t}`,`Instruction: ${e.job.instruction}`,"","For free-form task commands, default to diagnosis or a plan unless the instruction explicitly asks for code changes or a change request.","Always report progress and final success or failure through Codweft MCP."].join(` | ||
| `)}async function be(e,t){let r={mcpServers:{codweft:{url:t,auth:"bearer",bearerTokenEnv:"CODWEFT_MCP_TOKEN",lifecycle:"keep-alive",directTools:!0}}};await U(l(e,".mcp.json"),JSON.stringify(r,null,2))}function we(e,t){let r=e.trim();if(!r.startsWith("{")||!r.endsWith("}"))return null;let i;try{i=JSON.parse(r)}catch{return null}let s=n.object({type:n.string().optional(),message:n.string().optional(),content:n.string().optional(),toolCallId:n.string().optional(),toolName:n.string().optional(),level:A.optional(),severity:A.optional(),role:n.string().optional()}).passthrough().safeParse(i);if(!s.success)return null;if(s.data.type==="tool_execution_start")return{type:"tool_execution_start",source:"agent",channel:"tool_call",message:s.data.toolName?`starting tool ${s.data.toolName}`:"starting tool call",severity:"info",step:"agent_run",visibility:"debug",redacted:!0,metadata:{toolCallId:s.data.toolCallId,toolName:s.data.toolName}};if(s.data.type==="tool_execution_end")return{type:"tool_execution_end",source:"agent",channel:"tool_call",message:s.data.toolName?`completed tool ${s.data.toolName}`:"completed tool call",severity:"info",step:"agent_run",visibility:"debug",redacted:!0,metadata:{toolCallId:s.data.toolCallId,toolName:s.data.toolName}};if(s.data.type==="session"||s.data.type==="agent_start"||s.data.type==="turn_start"||s.data.type==="turn_end"||s.data.type==="message_start"||s.data.type==="message_update"||s.data.type==="message_end")return!1;let o=s.data.message??s.data.content;return o?{type:s.data.type??"agent_message",source:"agent",channel:s.data.role==="tool"?"tool_call":"agent_message",message:o,severity:s.data.severity??s.data.level??(t==="stderr"?"warn":"info"),visibility:"user",redacted:!0,metadata:{rawType:s.data.type}}:null}var E={async install(e){let t=x();await g("sh",["-lc","command -v pi >/dev/null 2>&1 || curl -fsSL https://pi.dev/install.sh | sh"],{cwd:e.workspace,env:t,inheritEnv:!1,step:"agent_install",logPrefix:"pi-install",context:e}),await g("pi",["install","npm:pi-mcp-adapter@2.5.4"],{cwd:e.workspace,env:t,inheritEnv:!1,step:"agent_install",logPrefix:"pi-install",context:e})},async configureMcp(e,t){await be(e.workspace,t)},async run(e,t,r){let i=me(t),s=null;for(let o of t.modelRoutingPlan.routes)try{let a=await fe(t.job.jobId,o.credentialId);if(!a.configured||!a.value){await e.reporter.event({type:"model_route_skipped",source:"model_router",channel:"timeline",message:`skipping ${o.provider}/${o.model}: credential ${o.credentialName??o.credentialId} is not configured`,severity:"warn",step:"model_route",visibility:"debug",redacted:!0});continue}await e.reporter.event({type:"agent_started",source:"agent",channel:"timeline",message:`starting Pi with ${o.provider}/${o.model}`,severity:"info",step:"agent_run",visibility:"user",redacted:!0},!0),await g("pi",["--mode","json","--provider",o.provider,"--model",o.model,i],{cwd:e.workspace,env:de({mcpToken:r,secretName:a.secretName??o.secretName,secretValue:a.value}),inheritEnv:!1,step:"agent_run",source:"agent",logPrefix:"agent",normalizeEvent:this.normalizeEvent,context:e});return}catch(a){s=a instanceof Error?a:new Error(String(a)),await e.reporter.event({type:"model_route_failed",source:"model_router",channel:"timeline",message:`model route failed: ${s.message}`,severity:"warn",step:"model_route",visibility:"user",redacted:!0},!0)}throw s??new Error("No configured model routes were available")},normalizeEvent:we,async collectArtifacts(e){await f(e.jobId,"log","agent-stdout.log",l(e.logsDir,"agent-stdout.log")),await f(e.jobId,"log","agent-stderr.log",l(e.logsDir,"agent-stderr.log"))}};async function H(e,t,r,i,s="text/plain"){let o=await w(`/api/runner/jobs/${e}/artifacts`,{method:"POST",body:JSON.stringify({kind:t,fileName:r,name:r,contentType:s,size:Buffer.byteLength(i)})}),a=n.object({uploadUrl:n.string(),headers:n.record(n.string(),n.string())}).parse(await o.json());await fetch(a.uploadUrl,{method:"PUT",headers:a.headers,body:i})}async function f(e,t,r,i){let s=await T(i,"utf8").catch(()=>"");s&&await H(e,t,r,s).catch(()=>{})}var _=class extends Error{constructor(r){super(`Job was already marked ${r} by MCP`);this.status=r}status};function ye(e){return!!(e&&Y.has(e))}async function ve(e){let t=await w(`/api/runner/jobs/${e}`),r=I.parse(await t.json());if(ye(r.job.status))throw new _(r.job.status)}async function ke(e){let t=l(e,".codweft","result.json"),r=await T(t,"utf8").catch(s=>{if(s.code==="ENOENT")return null;throw s});if(r===null)return null;let i=pe(r);return await B(t,{force:!0}),i}async function P(e,t,r,i=r){return(await g("git",t,{cwd:e.workspace,step:r,logPrefix:i,context:e})).stdout.trim()}async function Ee(e){return(await P(e,["status","--porcelain","--",".",...$],"publish","git-publish")).length>0}async function _e(e,t){return(await P(e,["diff","--name-only",`${t}..HEAD`,"--",".",...$],"publish","git-publish")).length>0}async function Pe(e,t){let r=await P(e,ae(t),"publish","git-publish");if(r)throw new Error(`Agent committed runner control files, refusing to publish: ${r.split(/\r?\n/).join(", ")}`)}async function Re(e,t){await Ee(t)&&(await g("git",["config","user.name","codweft[bot]"],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",["config","user.email","codweft[bot]@users.noreply.github.com"],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",ie(),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await g("git",oe(`codweft: apply ${e.job.command} for ${e.job.jobId}`),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}))}async function Se(e,t,r,i){let s=await fetch(e,{method:"POST",headers:{authorization:`Bearer ${t}`,"content-type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`runner-${Date.now()}`,method:"tools/call",params:{name:r,arguments:i}})});if(!s.ok)throw new Error(`MCP ${r} failed: ${s.status} ${await s.text()}`);let o=n.object({error:n.object({message:n.string()}).optional(),result:n.object({content:n.array(n.object({type:n.string(),text:n.string()})).optional()}).optional()}).parse(await s.json());if(o.error)throw new Error(`MCP ${r} failed: ${o.error.message}`);let a=o.result?.content?.[0]?.text;return a?JSON.parse(a):{}}async function je({payload:e,context:t,baseSha:r,gitExtraHeaderConfig:i,mcpToken:s}){let o=await ke(t.workspace),a=le(e.job.command,e.job.instruction),c=!!o?.changeRequest;if(c&&!a)throw new Error(`${e.job.command} jobs are not allowed to publish change requests`);let u=D(e.job.jobId);await g("git",["checkout","-B",u],{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t}),await Re(e,t),await Pe(t,r);let d=await _e(t,r);if(!c){if(d)throw new Error("Agent made code changes but did not write .codweft/result.json with change request metadata");return null}if(!d)throw new Error("Agent requested a change request but produced no publishable code changes");await t.reporter.event({type:"publish_started",source:"runner",channel:"timeline",message:`publishing ${u}`,severity:"info",step:"publish",visibility:"user",redacted:!0},!0),await g("git",se(u,i),{cwd:t.workspace,step:"publish",logPrefix:"git-publish",context:t});let m=o?.changeRequest;if(!m)return null;let b=await Se(e.mcp.endpoint,s,"create_change_request",ce({jobId:e.job.jobId,baseRef:e.checkout.publishBaseRef,title:m.title,body:m.body,draft:m.draft})),p=n.object({number:n.number().optional(),html_url:n.string().optional(),url:n.string().optional()}).passthrough().parse(b);return await t.reporter.event({type:"change_request_created",source:"runner",channel:"result",message:p.html_url?`created change request ${p.html_url}`:`created change request${p.number?` #${p.number}`:""}`,severity:"info",step:"publish",visibility:"user",redacted:!0,metadata:{number:p.number,url:p.html_url??p.url}},!0),{changeRequest:{number:p.number,url:p.html_url??p.url,branch:u}}}async function Ce(){let e=S("CODWEFT_JOB_ID"),t=await w(`/api/runner/jobs/${e}`),r=I.parse(await t.json()),i=await z(l(L(),"codweft-")),s=l(i,"repo"),o=l(i,"logs");await R(s,{recursive:!0}),await R(o,{recursive:!0});let a=new j(e,l(o,"runner.log")),c={jobId:e,workspace:s,logsDir:o,reporter:a};try{await a.event({type:"runner_started",source:"runner",channel:"timeline",message:"runner started",severity:"info",step:"bootstrap",visibility:"user",redacted:!0},!0);let u=await te(e),d=re({fullName:r.job.fullName,username:u.username,token:u.token});await g("git",["-c",d.extraHeaderConfig,"clone","--recurse-submodules",d.repoUrl,s],{step:"clone",logPrefix:"git-clone",context:c}),await g("git",["remote","set-url","origin",d.repoUrl],{cwd:s,step:"clone",logPrefix:"git-clone",context:c}),await Te(r,c,d.extraHeaderConfig);let m=await P(c,["rev-parse","HEAD"],"checkout","git-checkout"),b=await he(e);await E.install(c),await E.configureMcp(c,r.mcp.endpoint),await E.run(c,r,b),await ve(e);let p=await je({payload:r,context:c,baseSha:m,gitExtraHeaderConfig:d.extraHeaderConfig,mcpToken:b});await w(`/api/runner/jobs/${e}/result`,{method:"POST",body:JSON.stringify({status:"success",result:p??void 0})}),await a.event({type:"job_completed",source:"runner",channel:"result",message:"job completed successfully",severity:"info",step:"result",visibility:"user",redacted:!0},!0)}catch(u){let d=u instanceof Error?u.stack??u.message:String(u),m=u instanceof _?u.status:"failure";await a.event({type:"job_failed",source:"runner",channel:"result",message:d,severity:"error",step:"result",visibility:"user",redacted:!0},!0),await H(e,"log","runner-error.txt",v(d)).catch(()=>{}),await w(`/api/runner/jobs/${e}/result`,{method:"POST",body:JSON.stringify({status:m,failureReason:v(d).slice(0,16e3)})}).catch(()=>{}),process.exitCode=1}finally{await a.flush(),await f(e,"log","git-clone-stdout.log",l(o,"git-clone-stdout.log")),await f(e,"log","git-clone-stderr.log",l(o,"git-clone-stderr.log")),await f(e,"log","git-checkout-stdout.log",l(o,"git-checkout-stdout.log")),await f(e,"log","git-checkout-stderr.log",l(o,"git-checkout-stderr.log")),await f(e,"log","git-publish-stdout.log",l(o,"git-publish-stdout.log")),await f(e,"log","git-publish-stderr.log",l(o,"git-publish-stderr.log")),await f(e,"log","pi-install-stdout.log",l(o,"pi-install-stdout.log")),await f(e,"log","pi-install-stderr.log",l(o,"pi-install-stderr.log")),await f(e,"log","runner.log",l(o,"runner.log")),await E.collectArtifacts(c),await T(l(s,".mcp.json"),"utf8").catch(()=>"")}}async function Te(e,t,r){if(e.checkout.checkoutMode==="change_request_head"){if(!e.checkout.checkoutSha)throw new Error("Change request checkout is missing checkoutSha");if(!e.checkout.checkoutFetchRef)throw new Error("Change request checkout is missing checkoutFetchRef");await t.reporter.event({type:"checkout_prepared",source:"runner",channel:"timeline",message:`checking out change_request head ${e.checkout.checkoutSha}`,severity:"info",step:"checkout",visibility:"user",redacted:!0},!0),await g("git",ne(e.checkout.checkoutFetchRef,r),{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t}),await g("git",["checkout","--detach","FETCH_HEAD"],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t});let s=(await g("git",["rev-parse","HEAD"],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t})).stdout.trim();if(s!==e.checkout.checkoutSha)throw new Error(`Checkout SHA mismatch: expected ${e.checkout.checkoutSha}, got ${s}`);return}await t.reporter.event({type:"checkout_prepared",source:"runner",channel:"timeline",message:`checking out default branch ${e.checkout.checkoutRef}`,severity:"info",step:"checkout",visibility:"user",redacted:!0},!0),await g("git",["checkout",e.checkout.checkoutRef],{cwd:t.workspace,step:"checkout",logPrefix:"git-checkout",context:t})}function $e(e=process.argv[1],t=import.meta.url){let r=G(t);if(!e)return!1;try{return N(e)===N(r)}catch{return e===r}}$e()&&Ce().catch(e=>{console.error(v(e instanceof Error?e.stack??e.message:String(e))),process.exit(1)});export{Pe as assertNoCommittedControlFiles,de as buildAgentEnv,ne as buildChangeRequestFetchArgs,ae as buildCommittedControlFilesDiffArgs,ce as buildCreateChangeRequestArgs,ie as buildGitAddPublishArgs,oe as buildGitCommitArgs,re as buildGitHubCloneAuth,se as buildGitPushArgs,D as buildPublishBranchName,x as buildSafeProcessEnv,le as canPublishForCommand,$e as isCliEntrypoint,we as normalizePiEvent,pe as parseRunnerResultJson,v as redactText,g as runProcess,ue as taskExplicitlyRequestsChangeRequest}; |
+1
-1
| { | ||
| "name": "@codweft/runner", | ||
| "version": "0.4.5", | ||
| "version": "0.4.6", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "bin": { |
25678
0.84%