allwright 1.21.1 → 1.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/allwright.js +1 -1
  2. package/package.json +1 -1
package/bin/allwright.js CHANGED
@@ -752,4 +752,4 @@ _(requested by ${h.requestedBy} via Allwright)_`:"")),g.info(`Board action on ${
752
752
  ---
753
753
  _Created via Allwright Compose`+(!y.authored&&h.requestedBy?` by ${h.requestedBy}`:"")+", drafted with the project agent._",{key:D}=await y.jira.createIssue(m.project.jiraProjectKey,h.summary,(h.description||"(no description)")+C,h.asEpic?"Epic":"Task");h.files.length&&await y.jira.addAttachments(D,h.files.map(K=>({name:K.name,content:Buffer.from(K.contentBase64,"base64")}))).then(()=>g.info(`Compose attached ${h.files.length} file(s) to ${D}`)).catch(K=>g.warn(`Compose could not attach files to ${D}: ${K instanceof Error?K.message:K}`));let V=[];if(h.asEpic){for(let K of h.children){let ie=await y.jira.createIssue(m.project.jiraProjectKey,K.summary,K.description||"(no description)","Task",D);V.push(ie.key)}g.info(`Compose created epic ${D} with ${V.length} child ticket(s)`),_.sendChatCreateTicketResult(h.requestId,{ok:!0,issueKey:D,childKeys:V});return}h.triggerComment.trim()&&await y.jira.addTriggerComment(D,`${h.triggerComment.trim()}`+(!y.authored&&h.requestedBy?`
754
754
 
755
- _(requested by ${h.requestedBy} via Allwright)_`:"")),g.info(`Compose created ${D}`+(h.requestedBy?` for ${h.requestedBy}`:"")+(h.triggerComment?` with trigger "${h.triggerComment.trim()}"`:"")),_.sendChatCreateTicketResult(h.requestId,{ok:!0,issueKey:D})}catch(y){_.sendChatCreateTicketResult(h.requestId,{ok:!1,issueKey:"",error:y instanceof Error?y.message:String(y)})}},onAuditStart:async h=>{let m=x(h.projectId);if(!m){_.sendAuditStartResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{g.info(`Audit (${h.role}) starting on ${m.project.repoName}`+(h.scope.kind!=="repo"?` scoped to ${h.scope.kind}:${h.scope.ref}`:""));let y=Je(r,m.project,"assist");if("error"in y){_.sendAuditStartResult(h.requestId,{ok:!1,error:y.error});return}let C=await Cn({projectId:h.projectId,repoPath:m.repoPath,role:h.role,scope:h.scope,model:h.model,agent:y,jira:s(m.project.repoName)});if(!C.ok){_.sendAuditStartResult(h.requestId,{ok:!1,error:C.error});return}_.sendAuditStartResult(h.requestId,{ok:!0,audit:C.audit})}catch(y){_.sendAuditStartResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onAuditList:async h=>{try{_.sendAuditListResult(h.requestId,{ok:!0,audits:Au(h.projectId)})}catch(m){_.sendAuditListResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditGet:async h=>{try{let m=ju(h.projectId,h.auditId);if(!m){_.sendAuditGetResult(h.requestId,{ok:!1,error:"audit not found on this runner"});return}_.sendAuditGetResult(h.requestId,{ok:!0,audit:m})}catch(m){_.sendAuditGetResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditReview:async h=>{try{let m=Bo(h.projectId,h.auditId,{selected:h.selected,ticketed:h.ticketed,verdicts:h.verdicts,verdictBy:h.verdictBy});if(!m){_.sendAuditReviewResult(h.requestId,{ok:!1,error:"audit not found on this runner"});return}let y=x(h.projectId);if(y&&h.verdicts.length){let C=[];for(let D of h.verdicts){if(D.verdict==="valid"||m.lessonsWritten.includes(D.findingId))continue;let V=m.findings.find(K=>K.id===D.findingId);V&&(Go(y.repoPath,m.role,Lu(V,D.verdict,D.note,h.verdictBy)),C.push(D.findingId),g.info(`Lesson written (${m.role}): ${D.verdict} - ${V.title.slice(0,60)}`))}C.length&&(m=Bo(h.projectId,h.auditId,{markLessonsWritten:C})??m)}_.sendAuditReviewResult(h.requestId,{ok:!0,audit:m})}catch(m){_.sendAuditReviewResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onKnowledgeList:async h=>{let m=x(h.projectId);if(!m){_.sendKnowledgeListResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{_.sendKnowledgeListResult(h.requestId,{ok:!0,files:qu(m.repoPath)})}catch(y){_.sendKnowledgeListResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onKnowledgeSave:async h=>{let m=x(h.projectId);if(!m){_.sendKnowledgeSaveResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{h.append?Go(m.repoPath,h.role,Uu(h.content,h.by),h.level):Fu(m.repoPath,h.role,h.level,h.source,h.name,h.content),g.info(`Knowledge ${h.append?"taught":"saved"}: ${h.role}/${h.level}/${h.name}`),_.sendKnowledgeSaveResult(h.requestId,{ok:!0})}catch(y){_.sendKnowledgeSaveResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onEpicList:async h=>{let m=x(h.projectId);if(!m){_.sendEpicListResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{let y=s(m.project.repoName);if(h.epicKey){let C=await y.epicChildren(h.epicKey);_.sendEpicListResult(h.requestId,{ok:!0,children:C})}else{let C=await y.searchEpics(m.project.jiraProjectKey);_.sendEpicListResult(h.requestId,{ok:!0,epics:C})}}catch(y){_.sendEpicListResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onEpicSuggest:async h=>{let m=x(h.projectId);if(!m){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{let y=s(m.project.repoName),C=await y.epicChildren(h.epicKey),D=bu(C);if(!D.length){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:`every child of ${h.epicKey} is already done or in an agent's hands - nothing to sequence`});return}if(D.length===1){_.sendEpicSuggestResult(h.requestId,{ok:!0,issueKey:D[0].key,reason:"It's the only pending ticket left in this epic.",considered:1});return}let V=await Promise.all(D.slice(0,12).map(async re=>{try{let oe=await y.getIssue(re.key);return{...re,description:oe.description}}catch{return{...re,description:""}}})),K=(await y.getIssue(h.epicKey).catch(()=>null))?.summary??"";g.info(`Epic suggest: weighing ${V.length} pending children of ${h.epicKey}`);let ie=Je(r,m.project,"assist");if("error"in ie){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:ie.error});return}let W=await ie.adapter.run({repoPath:m.repoPath,prompt:wu(h.epicKey,K,V),timeoutSeconds:150,bin:ie.bin,mode:"read_only",maxTurns:20,sessionId:(0,qi.randomUUID)(),model:h.model||"claude-sonnet-5",onLog:()=>{}}),M=_u(W.resultText,V.map(re=>re.key));if("error"in M){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:M.error});return}g.info(`Epic suggest: ${h.epicKey} -> do ${M.issueKey} first`),_.sendEpicSuggestResult(h.requestId,{ok:!0,issueKey:M.issueKey,reason:M.reason,considered:V.length})}catch(y){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onTeamCreds:async h=>{try{let m=pe()??r;if(m.github?.token&&!m.github.login)try{let{GitHubClient:C}=await Promise.resolve().then(()=>(Ye(),zr));m.github.login=await new C(m.github.token).whoami(),he(m)}catch{}let y=fu(m,h.emails);_.sendTeamCredsResult(h.requestId,{ok:!0,...y})}catch(m){_.sendTeamCredsResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupStatus:async h=>{try{let m=pe()??r,{buildSetupReport:y}=await Promise.resolve().then(()=>(yh(),gh)),C=await y(m,h.repoNames);_.sendSetupStatusResult(h.requestId,{ok:!0,...C})}catch(m){_.sendSetupStatusResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupClaudeMdGenerate:async h=>{try{let m=x(h.projectId);if(!m){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=h.kind==="deploy"?"DEPLOY.md":h.kind==="design"?"DESIGN.md":"CLAUDE.md";g.info(`Setup: drafting ${y} for ${m.project.repoName}`);let C="Explore this repository and draft the content of a CLAUDE.md file for its repo root - the memory file AI agents read before working on tickets here. Cover: what the product does (one paragraph), the tech stack, how the code is organized (key directories and what lives in them), exact commands to install dependencies / build / run tests, code conventions worth following, and gotchas a newcomer would trip over. Under 150 lines, factual, no marketing language, nothing you cannot verify from the code. Reply with ONLY the file content - no preamble, no fences around the whole thing.",D='Inspect this repository (deploy scripts, CI files, server configs, package scripts) and draft the content of a DEPLOY.md deploy contract for its repo root. An automation tool will follow it LITERALLY, so only write what you can verify from the repo. IMPORTANT: you are running ON the machine where deploys will execute - commands are plain local commands, no ssh wrapper, unless the repo clearly documents a different target machine. For each environment that VERIFIABLY exists: name, branch, exact deploy commands, secrets as $NAMES only (never values), pre/post checks (post-restart checks must wait and retry - services report active before their port binds). Where you cannot verify something (hosts, service names, ports), write it with a clear "VERIFY:" marker so the human fills it in during review, instead of guessing. State explicitly which environments do NOT exist. Reply with ONLY the file content - no preamble, no fences around the whole thing.',V=`Explore this repository's UI code and draft the content of a DESIGN.md file for its repo root - the design guidelines AI agents will follow for anything user-facing here. This is a SEED DRAFT: extract only what the code proves, and leave taste to the humans. Cover, from real evidence (tailwind/theme configs, design tokens, CSS, component libraries, existing screens): the color palette actually in use (names + hex), typography (families, the size scale), spacing and layout conventions, the component idiom (which library, how buttons/forms/cards are built, naming), dark mode, responsive breakpoints, and accessibility patterns already present. For everything code cannot prove - brand intent, tone of voice, look-and-feel references, what to never do - write a line starting with "VERIFY:" phrased as a question for the human to answer during review, instead of guessing. If the project has no UI at all, say exactly that in one line and stop. Under 120 lines, factual, no marketing language. Reply with ONLY the file content - no preamble, no fences around the whole thing.`,K=Je(r,m.project,"assist");if("error"in K){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,content:"",error:K.error});return}let W=(await K.adapter.run({repoPath:m.repoPath,prompt:h.kind==="deploy"?D:h.kind==="design"?V:C,timeoutSeconds:240,bin:K.bin,mode:"read_only",maxTurns:40,sessionId:(0,qi.randomUUID)(),onLog:()=>{}})).resultText.trim();if(!W){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:"the agent returned no content - try again"});return}_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!0,content:W})}catch(m){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupClaudeMdSave:async h=>{try{let m=x(h.projectId);if(!m){_.sendSetupClaudeMdSaveResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=h.kind==="deploy"?"DEPLOY.md":h.kind==="design"?"DESIGN.md":"CLAUDE.md",C=await Ua({repoPath:m.repoPath,relPath:y,content:h.content,overwrite:h.overwrite,commitMessage:`Add ${y} (agent-drafted, human-approved via Allwright)`});C.ok&&g.info(`Setup: wrote human-approved ${y} to ${m.repoPath}`),_.sendSetupClaudeMdSaveResult(h.requestId,C)}catch(m){_.sendSetupClaudeMdSaveResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onRepoDocSave:async h=>{try{let m=x(h.projectId);if(!m){_.sendRepoDocSaveResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=await Ua({repoPath:m.repoPath,relPath:h.relPath,content:h.content,overwrite:h.overwrite,commitMessage:`Add ${h.relPath.trim()} (written in Allwright Compose, human-approved)`});y.ok&&g.info(`Saved approved doc ${h.relPath} into ${m.repoPath}`),_.sendRepoDocSaveResult(h.requestId,y)}catch(m){_.sendRepoDocSaveResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditStats:h=>{try{_.sendAuditStatsResult(h.requestId,{ok:!0,byProject:$u(h.projectIds,h.since)})}catch(m){_.sendAuditStatsResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onRunnerUpdate:async h=>{if(!process.env.INVOCATION_ID){_.sendRunnerUpdateResult(h.requestId,{ok:!1,error:"this runner is not running as a systemd service, so it cannot restart itself after updating. On the machine run: allwright update && allwright stop && allwright start"});return}let m=()=>{g.info("Installing allwright@latest");let C=Un.default.dirname(process.execPath),D=et.default.existsSync(Un.default.join(C,"npm"))?Un.default.join(C,"npm"):"npm";(0,Et.execFile)(D,["install","-g","allwright@latest"],{timeout:18e4,env:{...process.env,PATH:`${C}:${process.env.PATH??""}`}},(V,K,ie)=>{if(V){_.sendRunnerUpdateResult(h.requestId,{ok:!1,error:`npm install failed: ${(ie||V.message).slice(0,300)} - on the machine run: allwright update && sudo systemctl restart allwright`});return}_.sendRunnerUpdateResult(h.requestId,{ok:!0,note:"updated - restarting now, back in a few seconds"}),g.info("Update installed; exiting so systemd restarts on the new version"),setTimeout(()=>process.exit(0),1500)})};if(p.idle){m();return}if(v){_.sendRunnerUpdateResult(h.requestId,{ok:!0,scheduled:!0,note:"an update is already scheduled and will install once the current work finishes"});return}let y=p.busyCount;_.sendRunnerUpdateResult(h.requestId,{ok:!0,scheduled:!0,note:`scheduled - ${y} run(s)/deploy(s) active right now. Nothing is refused meanwhile; the install runs automatically as soon as this machine goes idle.`}),g.info(`Update scheduled: waiting for ${y} active run(s)/deploy(s) to finish`),v=setInterval(()=>{p.idle&&(clearInterval(v),v=null,m())},5e3)},onResolveConflict:async h=>{let m=y=>_.sendResolveConflictResult(h.requestId,{ok:!1,note:y});try{let y=is(h.runId);if(!y){m("this runner no longer has that PR on file - it may have been decided already");return}let C=r.repos[y.repoName];if(!C){m(`no local checkout mapped for "${y.repoName}" on this machine`);return}let D=Ae(r,y.repoName);if(!D){m("no GitHub token on this runner for that repo");return}let V=await new $e(D).getPullRequest(y.owner,y.repo,y.prNumber);if(!V){m("that pull request no longer exists on GitHub");return}if(!V.baseRef){m("GitHub did not report the PR's base branch, so there is nothing to merge in");return}let K=u.find(re=>re.repoName===y.repoName),ie=0,W=(re,oe)=>_.send({type:"run.log",runId:h.runId,seq:Date.now()*10+ie++%10,ts:new Date().toISOString(),level:re,message:oe}),M=await Bc({cfg:r,repoPath:C.path,branch:y.branch,baseBranch:V.baseRef,issueKey:y.issueKey,agentId:K?.defaultAgent||Ht.DEFAULT_AGENT_ID,configDir:y.agentConfigDir,timeoutSeconds:900,onLog:W});M.ok&&M.headSha&&(y.headSha=M.headSha,y.conflictNotified=!1,Bt(y),_.send({type:"run.pr.conflict",runId:h.runId,conflicted:!1}),_.send({type:"run.status",runId:h.runId,status:"awaiting_approval",headSha:M.headSha,...M.costUsd!==void 0?{costUsd:M.costUsd}:{},...M.inputTokens!==void 0?{inputTokens:M.inputTokens}:{},...M.outputTokens!==void 0?{outputTokens:M.outputTokens}:{}})),_.sendResolveConflictResult(h.requestId,M)}catch(y){m(y instanceof Error?y.message.slice(0,300):String(y))}},onAgentProfiles:h=>{try{let m=C=>{try{return et.default.existsSync(C)&&et.default.readdirSync(C).length>0}catch{return!1}},y=Object.entries(r.claudeProfiles).map(([C,D])=>({agent:"claude-code",name:C,loggedIn:m(D.configDir)}));_.sendAgentProfilesResult(h.requestId,{ok:!0,profiles:y,defaultLoggedIn:m(Un.default.join(Fa.default.homedir(),".claude")),agentsWithoutProfiles:Ht.REGISTERED_AGENTS.filter(C=>!C.capabilities.loginProfiles).map(C=>C.id)})}catch(m){_.sendAgentProfilesResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onJiraUsers:async h=>{try{let m=await t.searchUsers(h.query);_.sendJiraUsersResult(h.requestId,{ok:!0,users:m})}catch(m){_.sendJiraUsersResult(h.requestId,{ok:!1,users:[],error:m instanceof Error?m.message:String(m)})}}});S.transport=_,p=new wi(r,s,_);let J=Object.values(En()).filter(h=>h.kind==="implement").map(h=>h.repoPath);xo(Object.values(r.repos).map(h=>h.path),J).catch(()=>{}),_.start(),p.start();let O=()=>{g.info("shutting down"),v&&clearInterval(v),_.stop(),Rl(process.pid),co(),process.exit(0)},L=!1,Q=()=>{if(L){g.warn("second stop signal - exiting NOW; the interrupted run will be marked failed"),O();return}if(L=!0,p.stop(),p.idle){O();return}g.info(`stop requested - no new work will start; waiting for ${p.busyCount} active run(s)/deploy(s) to finish. Send the signal again to kill them immediately.`);let h=setInterval(()=>{p.idle&&(clearInterval(h),O())},3e3)};process.on("SIGINT",Q),process.on("SIGTERM",Q),process.on("SIGHUP",()=>{try{let h=ne();if(!h.jira)throw new Error("no Jira credentials in the reloaded config");for(let m of Object.keys(r))delete r[m];Object.assign(r,h),t=new Ee(h.jira),n.clear(),g.info("config reloaded live - new Jira tokens/repos/rules apply from the next poll. (Poll interval and control-plane URL still need a restart.)")}catch(h){g.error(`config reload failed - keeping the running config: ${h instanceof Error?h.message:String(h)}`)}})}ye.command("start").description("Start watching Jira. Runs in the background by default - close this terminal and it keeps going until you run `allwright stop`. Use --foreground to keep it attached (e.g. under systemd).").option("--foreground","run in this terminal instead of detaching to the background").action(async r=>{let e=ne();if(r.foreground){Iv(e);return}let t=dr();if(t&&hr(t)){g.info(`already running in the background (pid ${t}). Use \`allwright status\` or \`stop\`.`);return}let n=Hn();if(n){g.info(`a runner is already running on this machine (pid ${n}) - most likely the systemd service. One machine runs ONE runner; nothing to do. (Service controls: sudo systemctl status|stop|restart allwright)`);return}let s=Cl(),i=(0,Et.spawn)(process.execPath,[process.argv[1],"start","--foreground"],{detached:!0,stdio:["ignore",s,s]});i.unref(),Il(i.pid),g.info(`Runner started in the background (pid ${i.pid}).`),g.info(`Logs: ${Ms()}`),g.info("It will keep polling Jira until you run `allwright stop` - closing this terminal is fine.")});ye.command("stop").description("Stop the background runner started by `start`. Graceful by default: it stops taking new work and exits once active runs and deploys finish - safe to use while customers' tickets are running.").option("--force","kill immediately - anything executing is interrupted and marked failed").action(r=>{let e=dr();if(!e||!hr(e)){g.info("no background runner is running"),e&&et.default.rmSync(hn(),{force:!0});return}if(r.force){process.kill(e,"SIGKILL"),et.default.rmSync(hn(),{force:!0}),g.info(`killed (pid ${e}) - any interrupted run will show as failed on the dashboard`);return}process.kill(e,"SIGTERM"),g.info(`stop signal sent (pid ${e}) - it stops polling now and exits once active work finishes (watch \`allwright status\`). Use --force to kill immediately.`)});ye.command("status").description("Check whether the background runner is running").action(()=>{let r=dr();r&&hr(r)?g.info(`running in the background (pid ${r})`):g.info("not running")});ye.parseAsync().catch(r=>{g.error(r instanceof Error?r.message:String(r)),process.exit(1)});
755
+ _(requested by ${h.requestedBy} via Allwright)_`:"")),g.info(`Compose created ${D}`+(h.requestedBy?` for ${h.requestedBy}`:"")+(h.triggerComment?` with trigger "${h.triggerComment.trim()}"`:"")),_.sendChatCreateTicketResult(h.requestId,{ok:!0,issueKey:D})}catch(y){_.sendChatCreateTicketResult(h.requestId,{ok:!1,issueKey:"",error:y instanceof Error?y.message:String(y)})}},onAuditStart:async h=>{let m=x(h.projectId);if(!m){_.sendAuditStartResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{g.info(`Audit (${h.role}) starting on ${m.project.repoName}`+(h.scope.kind!=="repo"?` scoped to ${h.scope.kind}:${h.scope.ref}`:""));let y=Je(r,m.project,"assist");if("error"in y){_.sendAuditStartResult(h.requestId,{ok:!1,error:y.error});return}let C=await Cn({projectId:h.projectId,repoPath:m.repoPath,role:h.role,scope:h.scope,model:h.model,agent:y,jira:s(m.project.repoName)});if(!C.ok){_.sendAuditStartResult(h.requestId,{ok:!1,error:C.error});return}_.sendAuditStartResult(h.requestId,{ok:!0,audit:C.audit})}catch(y){_.sendAuditStartResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onAuditList:async h=>{try{_.sendAuditListResult(h.requestId,{ok:!0,audits:Au(h.projectId)})}catch(m){_.sendAuditListResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditGet:async h=>{try{let m=ju(h.projectId,h.auditId);if(!m){_.sendAuditGetResult(h.requestId,{ok:!1,error:"audit not found on this runner"});return}_.sendAuditGetResult(h.requestId,{ok:!0,audit:m})}catch(m){_.sendAuditGetResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditReview:async h=>{try{let m=Bo(h.projectId,h.auditId,{selected:h.selected,ticketed:h.ticketed,verdicts:h.verdicts,verdictBy:h.verdictBy});if(!m){_.sendAuditReviewResult(h.requestId,{ok:!1,error:"audit not found on this runner"});return}let y=x(h.projectId);if(y&&h.verdicts.length){let C=[];for(let D of h.verdicts){if(D.verdict==="valid"||m.lessonsWritten.includes(D.findingId))continue;let V=m.findings.find(K=>K.id===D.findingId);V&&(Go(y.repoPath,m.role,Lu(V,D.verdict,D.note,h.verdictBy)),C.push(D.findingId),g.info(`Lesson written (${m.role}): ${D.verdict} - ${V.title.slice(0,60)}`))}C.length&&(m=Bo(h.projectId,h.auditId,{markLessonsWritten:C})??m)}_.sendAuditReviewResult(h.requestId,{ok:!0,audit:m})}catch(m){_.sendAuditReviewResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onKnowledgeList:async h=>{let m=x(h.projectId);if(!m){_.sendKnowledgeListResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{_.sendKnowledgeListResult(h.requestId,{ok:!0,files:qu(m.repoPath)})}catch(y){_.sendKnowledgeListResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onKnowledgeSave:async h=>{let m=x(h.projectId);if(!m){_.sendKnowledgeSaveResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{h.append?Go(m.repoPath,h.role,Uu(h.content,h.by),h.level):Fu(m.repoPath,h.role,h.level,h.source,h.name,h.content),g.info(`Knowledge ${h.append?"taught":"saved"}: ${h.role}/${h.level}/${h.name}`),_.sendKnowledgeSaveResult(h.requestId,{ok:!0})}catch(y){_.sendKnowledgeSaveResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onEpicList:async h=>{let m=x(h.projectId);if(!m){_.sendEpicListResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{let y=s(m.project.repoName);if(h.epicKey){let C=await y.epicChildren(h.epicKey);_.sendEpicListResult(h.requestId,{ok:!0,children:C})}else{let C=await y.searchEpics(m.project.jiraProjectKey);_.sendEpicListResult(h.requestId,{ok:!0,epics:C})}}catch(y){_.sendEpicListResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onEpicSuggest:async h=>{let m=x(h.projectId);if(!m){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:"this runner has no local checkout mapped for that project"});return}try{let y=s(m.project.repoName),C=await y.epicChildren(h.epicKey),D=bu(C);if(!D.length){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:`every child of ${h.epicKey} is already done or in an agent's hands - nothing to sequence`});return}if(D.length===1){_.sendEpicSuggestResult(h.requestId,{ok:!0,issueKey:D[0].key,reason:"It's the only pending ticket left in this epic.",considered:1});return}let V=await Promise.all(D.slice(0,12).map(async re=>{try{let oe=await y.getIssue(re.key);return{...re,description:oe.description}}catch{return{...re,description:""}}})),K=(await y.getIssue(h.epicKey).catch(()=>null))?.summary??"";g.info(`Epic suggest: weighing ${V.length} pending children of ${h.epicKey}`);let ie=Je(r,m.project,"assist");if("error"in ie){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:ie.error});return}let W=await ie.adapter.run({repoPath:m.repoPath,prompt:wu(h.epicKey,K,V),timeoutSeconds:150,bin:ie.bin,mode:"read_only",maxTurns:20,sessionId:(0,qi.randomUUID)(),model:h.model||"claude-sonnet-5",onLog:()=>{}}),M=_u(W.resultText,V.map(re=>re.key));if("error"in M){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:M.error});return}g.info(`Epic suggest: ${h.epicKey} -> do ${M.issueKey} first`),_.sendEpicSuggestResult(h.requestId,{ok:!0,issueKey:M.issueKey,reason:M.reason,considered:V.length})}catch(y){_.sendEpicSuggestResult(h.requestId,{ok:!1,error:y instanceof Error?y.message:String(y)})}},onTeamCreds:async h=>{try{let m=pe()??r;if(m.github?.token&&!m.github.login)try{let{GitHubClient:C}=await Promise.resolve().then(()=>(Ye(),zr));m.github.login=await new C(m.github.token).whoami(),he(m)}catch{}let y=fu(m,h.emails);_.sendTeamCredsResult(h.requestId,{ok:!0,...y})}catch(m){_.sendTeamCredsResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupStatus:async h=>{try{let m=pe()??r,{buildSetupReport:y}=await Promise.resolve().then(()=>(yh(),gh)),C=await y(m,h.repoNames);_.sendSetupStatusResult(h.requestId,{ok:!0,...C})}catch(m){_.sendSetupStatusResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupClaudeMdGenerate:async h=>{try{let m=x(h.projectId);if(!m){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=h.kind==="deploy"?"DEPLOY.md":h.kind==="design"?"DESIGN.md":"CLAUDE.md";g.info(`Setup: drafting ${y} for ${m.project.repoName}`);let C="Explore this repository and draft the content of a CLAUDE.md file for its repo root - the memory file AI agents read before working on tickets here. Cover: what the product does (one paragraph), the tech stack, how the code is organized (key directories and what lives in them), exact commands to install dependencies / build / run tests, code conventions worth following, and gotchas a newcomer would trip over. Under 150 lines, factual, no marketing language, nothing you cannot verify from the code. Reply with ONLY the file content - no preamble, no fences around the whole thing.",D='Inspect this repository (deploy scripts, CI files, server configs, package scripts) and draft the content of a DEPLOY.md deploy contract for its repo root. An automation tool will follow it LITERALLY, so only write what you can verify from the repo. IMPORTANT: you are running ON the machine where deploys will execute - commands are plain local commands, no ssh wrapper, unless the repo clearly documents a different target machine. For each environment that VERIFIABLY exists: name, branch, exact deploy commands, secrets as $NAMES only (never values), pre/post checks (post-restart checks must wait and retry - services report active before their port binds). Where you cannot verify something (hosts, service names, ports), write it with a clear "VERIFY:" marker so the human fills it in during review, instead of guessing. State explicitly which environments do NOT exist. Reply with ONLY the file content - no preamble, no fences around the whole thing.',V=`Explore this repository's UI code and draft the content of a DESIGN.md file for its repo root - the design guidelines AI agents will follow for anything user-facing here. This is a SEED DRAFT: extract only what the code proves, and leave taste to the humans. Cover, from real evidence (tailwind/theme configs, design tokens, CSS, component libraries, existing screens): the color palette actually in use (names + hex), typography (families, the size scale), spacing and layout conventions, the component idiom (which library, how buttons/forms/cards are built, naming), dark mode, responsive breakpoints, and accessibility patterns already present. For everything code cannot prove - brand intent, tone of voice, look-and-feel references, what to never do - write a line starting with "VERIFY:" phrased as a question for the human to answer during review, instead of guessing. If the project has no UI at all, say exactly that in one line and stop. Under 120 lines, factual, no marketing language. Reply with ONLY the file content - no preamble, no fences around the whole thing.`,K=Je(r,m.project,"assist");if("error"in K){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,content:"",error:K.error});return}let W=(await K.adapter.run({repoPath:m.repoPath,prompt:h.kind==="deploy"?D:h.kind==="design"?V:C,timeoutSeconds:240,bin:K.bin,mode:"read_only",maxTurns:40,sessionId:(0,qi.randomUUID)(),onLog:()=>{}})).resultText.trim();if(!W){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:"the agent returned no content - try again"});return}_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!0,content:W})}catch(m){_.sendSetupClaudeMdGenerateResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onSetupClaudeMdSave:async h=>{try{let m=x(h.projectId);if(!m){_.sendSetupClaudeMdSaveResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=h.kind==="deploy"?"DEPLOY.md":h.kind==="design"?"DESIGN.md":"CLAUDE.md",C=await Ua({repoPath:m.repoPath,relPath:y,content:h.content,overwrite:h.overwrite,commitMessage:`Add ${y} (agent-drafted, human-approved via Allwright)`});C.ok&&g.info(`Setup: wrote human-approved ${y} to ${m.repoPath}`),_.sendSetupClaudeMdSaveResult(h.requestId,C)}catch(m){_.sendSetupClaudeMdSaveResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onRepoDocSave:async h=>{try{let m=x(h.projectId);if(!m){_.sendRepoDocSaveResult(h.requestId,{ok:!1,error:"no local checkout mapped for that project"});return}let y=await Ua({repoPath:m.repoPath,relPath:h.relPath,content:h.content,overwrite:h.overwrite,commitMessage:`Add ${h.relPath.trim()} (written in Allwright Compose, human-approved)`});y.ok&&g.info(`Saved approved doc ${h.relPath} into ${m.repoPath}`),_.sendRepoDocSaveResult(h.requestId,y)}catch(m){_.sendRepoDocSaveResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onAuditStats:h=>{try{_.sendAuditStatsResult(h.requestId,{ok:!0,byProject:$u(h.projectIds,h.since)})}catch(m){_.sendAuditStatsResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onRunnerUpdate:async h=>{if(!process.env.INVOCATION_ID){_.sendRunnerUpdateResult(h.requestId,{ok:!1,error:"this runner is not running as a systemd service, so it cannot restart itself after updating. On the machine run: allwright update && allwright stop && allwright start"});return}let m=()=>{g.info("Installing allwright@latest");let C=Un.default.dirname(process.execPath),D=et.default.existsSync(Un.default.join(C,"npm"))?Un.default.join(C,"npm"):"npm";(0,Et.execFile)(D,["install","-g","allwright@latest"],{timeout:18e4,env:{...process.env,PATH:`${C}:${process.env.PATH??""}`}},(V,K,ie)=>{if(V){_.sendRunnerUpdateResult(h.requestId,{ok:!1,error:`npm install failed: ${(ie||V.message).slice(0,300)} - on the machine run: allwright update && sudo systemctl restart allwright`});return}_.sendRunnerUpdateResult(h.requestId,{ok:!0,note:"updated - restarting now, back in a few seconds"}),g.info("Update installed; exiting so systemd restarts on the new version"),setTimeout(()=>process.exit(0),1500)})};if(p.idle){m();return}if(v){_.sendRunnerUpdateResult(h.requestId,{ok:!0,scheduled:!0,note:"an update is already scheduled and will install once the current work finishes"});return}let y=p.busyCount;_.sendRunnerUpdateResult(h.requestId,{ok:!0,scheduled:!0,note:`scheduled - ${y} run(s)/deploy(s) active right now. Nothing is refused meanwhile; the install runs automatically as soon as this machine goes idle.`}),g.info(`Update scheduled: waiting for ${y} active run(s)/deploy(s) to finish`),v=setInterval(()=>{p.idle&&(clearInterval(v),v=null,m())},5e3)},onResolveConflict:async h=>{let m=y=>_.sendResolveConflictResult(h.requestId,{ok:!1,note:y});try{let y=is(h.runId);if(!y){m("this runner no longer has that PR on file - it may have been decided already");return}let C=r.repos[y.repoName];if(!C){m(`no local checkout mapped for "${y.repoName}" on this machine`);return}let D=Ae(r,y.repoName);if(!D){m("no GitHub token on this runner for that repo");return}let V=await new $e(D).getPullRequest(y.owner,y.repo,y.prNumber);if(!V){m("that pull request no longer exists on GitHub");return}if(!V.baseRef){m("GitHub did not report the PR's base branch, so there is nothing to merge in");return}let K=u.find(re=>re.repoName===y.repoName),ie=1e5,W=(re,oe)=>_.send({type:"run.log",runId:h.runId,seq:ie++,ts:new Date().toISOString(),level:re,message:oe}),M=await Bc({cfg:r,repoPath:C.path,branch:y.branch,baseBranch:V.baseRef,issueKey:y.issueKey,agentId:K?.defaultAgent||Ht.DEFAULT_AGENT_ID,configDir:y.agentConfigDir,timeoutSeconds:900,onLog:W});M.ok&&M.headSha&&(y.headSha=M.headSha,y.conflictNotified=!1,Bt(y),_.send({type:"run.pr.conflict",runId:h.runId,conflicted:!1}),_.send({type:"run.status",runId:h.runId,status:"awaiting_approval",headSha:M.headSha,...M.costUsd!==void 0?{costUsd:M.costUsd}:{},...M.inputTokens!==void 0?{inputTokens:M.inputTokens}:{},...M.outputTokens!==void 0?{outputTokens:M.outputTokens}:{}})),_.sendResolveConflictResult(h.requestId,M)}catch(y){m(y instanceof Error?y.message.slice(0,300):String(y))}},onAgentProfiles:h=>{try{let m=C=>{try{return et.default.existsSync(C)&&et.default.readdirSync(C).length>0}catch{return!1}},y=Object.entries(r.claudeProfiles).map(([C,D])=>({agent:"claude-code",name:C,loggedIn:m(D.configDir)}));_.sendAgentProfilesResult(h.requestId,{ok:!0,profiles:y,defaultLoggedIn:m(Un.default.join(Fa.default.homedir(),".claude")),agentsWithoutProfiles:Ht.REGISTERED_AGENTS.filter(C=>!C.capabilities.loginProfiles).map(C=>C.id)})}catch(m){_.sendAgentProfilesResult(h.requestId,{ok:!1,error:m instanceof Error?m.message:String(m)})}},onJiraUsers:async h=>{try{let m=await t.searchUsers(h.query);_.sendJiraUsersResult(h.requestId,{ok:!0,users:m})}catch(m){_.sendJiraUsersResult(h.requestId,{ok:!1,users:[],error:m instanceof Error?m.message:String(m)})}}});S.transport=_,p=new wi(r,s,_);let J=Object.values(En()).filter(h=>h.kind==="implement").map(h=>h.repoPath);xo(Object.values(r.repos).map(h=>h.path),J).catch(()=>{}),_.start(),p.start();let O=()=>{g.info("shutting down"),v&&clearInterval(v),_.stop(),Rl(process.pid),co(),process.exit(0)},L=!1,Q=()=>{if(L){g.warn("second stop signal - exiting NOW; the interrupted run will be marked failed"),O();return}if(L=!0,p.stop(),p.idle){O();return}g.info(`stop requested - no new work will start; waiting for ${p.busyCount} active run(s)/deploy(s) to finish. Send the signal again to kill them immediately.`);let h=setInterval(()=>{p.idle&&(clearInterval(h),O())},3e3)};process.on("SIGINT",Q),process.on("SIGTERM",Q),process.on("SIGHUP",()=>{try{let h=ne();if(!h.jira)throw new Error("no Jira credentials in the reloaded config");for(let m of Object.keys(r))delete r[m];Object.assign(r,h),t=new Ee(h.jira),n.clear(),g.info("config reloaded live - new Jira tokens/repos/rules apply from the next poll. (Poll interval and control-plane URL still need a restart.)")}catch(h){g.error(`config reload failed - keeping the running config: ${h instanceof Error?h.message:String(h)}`)}})}ye.command("start").description("Start watching Jira. Runs in the background by default - close this terminal and it keeps going until you run `allwright stop`. Use --foreground to keep it attached (e.g. under systemd).").option("--foreground","run in this terminal instead of detaching to the background").action(async r=>{let e=ne();if(r.foreground){Iv(e);return}let t=dr();if(t&&hr(t)){g.info(`already running in the background (pid ${t}). Use \`allwright status\` or \`stop\`.`);return}let n=Hn();if(n){g.info(`a runner is already running on this machine (pid ${n}) - most likely the systemd service. One machine runs ONE runner; nothing to do. (Service controls: sudo systemctl status|stop|restart allwright)`);return}let s=Cl(),i=(0,Et.spawn)(process.execPath,[process.argv[1],"start","--foreground"],{detached:!0,stdio:["ignore",s,s]});i.unref(),Il(i.pid),g.info(`Runner started in the background (pid ${i.pid}).`),g.info(`Logs: ${Ms()}`),g.info("It will keep polling Jira until you run `allwright stop` - closing this terminal is fine.")});ye.command("stop").description("Stop the background runner started by `start`. Graceful by default: it stops taking new work and exits once active runs and deploys finish - safe to use while customers' tickets are running.").option("--force","kill immediately - anything executing is interrupted and marked failed").action(r=>{let e=dr();if(!e||!hr(e)){g.info("no background runner is running"),e&&et.default.rmSync(hn(),{force:!0});return}if(r.force){process.kill(e,"SIGKILL"),et.default.rmSync(hn(),{force:!0}),g.info(`killed (pid ${e}) - any interrupted run will show as failed on the dashboard`);return}process.kill(e,"SIGTERM"),g.info(`stop signal sent (pid ${e}) - it stops polling now and exits once active work finishes (watch \`allwright status\`). Use --force to kill immediately.`)});ye.command("status").description("Check whether the background runner is running").action(()=>{let r=dr();r&&hr(r)?g.info(`running in the background (pid ${r})`):g.info("not running")});ye.parseAsync().catch(r=>{g.error(r instanceof Error?r.message:String(r)),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allwright",
3
- "version": "1.21.1",
3
+ "version": "1.21.2",
4
4
  "description": "Allwright — the AI teammate that runs on YOUR machine. Polls your Jira, runs jailed agents in your local checkouts, opens PRs, executes approved deploys, and audits security/testing/architecture. Credentials and code never leave your host.",
5
5
  "bin": {
6
6
  "allwright": "bin/allwright.js",