squad-kit 1.3.7 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +6 -5
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1704,7 +1704,7 @@ ${PLAIN2}`}function allStoryLines(b){return b.epics.flatMap(e=>[...e.features.fl
1704
1704
 
1705
1705
  ${documentText(b,documentFiles(ctx.bp,b))}`,r=await ask(ctx,{step:"questions",prompt:replyPrompt2(q4,b.questions.indexOf(q4)+1,said,context),...session?{resume:session}:{},timeoutMs:BRD_ANSWER_MS});return updateBreakdown(ctx.bp,ctx.id,x=>{let y=x.questions.find(z13=>z13.id===q4.id);y&&(y.thread=[...y.thread??[],{who:"analyst",text:r.text.trim()||"I have nothing to add: pick the option closest to what you mean.",at:now()}],y.thinking=!1)}),done(),!1}async function draftStep(ctx,b0){let docs=documentFiles(ctx.bp,b0),session=readBreakdownState(ctx.bp,ctx.id).sessions?.read;if(!b0.epics.length){setNow(ctx,"Writing the outline: epics and features");let r=await ask(ctx,{step:"spec",prompt:outlinePrompt(ctx.project,b0,docs,SKIP_CHOICE,!!session),schema:OUTLINE_SCHEMA,...session?{resume:session}:{},timeoutMs:BRD_WRITE_MS});session=readBreakdownState(ctx.bp,ctx.id).sessions?.read;let out18=r.structured,b2=updateBreakdown(ctx.bp,ctx.id,x=>{out18.summary?.trim()&&(x.summary=out18.summary.trim()),x.epics=(out18.epics??[]).map(e=>({key:nextKey(x,"epic"),title:String(e.title??"").trim()||"Untitled epic",...e.goal?.trim()?{goal:e.goal.trim()}:{},...cleanRef(e.ref)?{ref:cleanRef(e.ref)}:{},features:(e.features??[]).map(f=>({key:nextKey(x,"feature"),title:String(f.title??"").trim()||"Untitled feature",...f.summary?.trim()?{summary:f.summary.trim()}:{},...cleanRef(f.ref)?{ref:cleanRef(f.ref)}:{},stories:[]})),stories:[]})),x.leftOut=(out18.leftOut??[]).map(l=>({text:String(l.text??"").trim(),why:String(l.why??"").trim(),...cleanRef(l.ref)?{ref:cleanRef(l.ref)}:{}})).filter(l=>l.text);let by2=byFor(x);addDecided2(x,out18.decided,new Map,by2);for(let q4 of x.questions){if(q4.answer?.choice!==SKIP_CHOICE)continue;(out18.decided??[]).some(d=>String(d.question??"").trim().toLowerCase()===q4.id)||x.decided.push({id:nextDecidedId(x.decided),text:`${q4.text} The Analyst chose: ${q4.options[0]??"its own best guess"}.`,by:"analyst",at:now()})}});if(!b2.epics.length)throw new StepError("The Analyst found no epics in the document.");updateBreakdownState(ctx.bp,ctx.id,s=>{addBreakdownActivity(s,"analyst",`Wrote the outline: ${b2.epics.length} ${b2.epics.length===1?"epic":"epics"} and ${counts(b2).features} features`),s.job&&(s.job.found=counts(b2))})}for(;;){goOn2(ctx);let b2=readBreakdown(ctx.bp,ctx.id),i=b2.epics.findIndex(e=>!e.stories.length&&!e.features.some(f=>f.stories.length));if(i<0)break;let epic=b2.epics[i];setNow(ctx,`Writing the stories of ${epic.title} (epic ${i+1} of ${b2.epics.length})`,{found:counts(b2)});let first2=b2.next.story,r=await ask(ctx,{step:"spec",prompt:storiesPrompt(ctx.project,b2,epic,i+1,b2.epics.length,first2,docs,SKIP_CHOICE,!!session),schema:STORIES_SCHEMA,...session?{resume:session}:{},timeoutMs:BRD_WRITE_MS});session=readBreakdownState(ctx.bp,ctx.id).sessions?.read;let out18=r.structured,made=0,after=updateBreakdown(ctx.bp,ctx.id,x=>{let e=x.epics.find(y=>y.key===epic.key);if(!e)return;let map=new Map,list2=[];for(let o of out18.stories??[]){let key=nextKey(x,"story");o.key&&map.set(String(o.key).trim().toUpperCase(),key),list2.push({s:story(o,key),needs:o.needs??[],feature:o.feature?String(o.feature).trim().toUpperCase():void 0})}for(let{s,feature}of list2){let f=e.features.find(y=>y.key===feature);(f?f.stories:e.stories).push(s)}resolveNeeds(x,list2,map),cutLoops(x),addDecided2(x,out18.decided,map,byFor(x)),made=list2.length});if(!made)throw new StepError(`The Analyst wrote no stories for ${epic.title}.`);updateBreakdownState(ctx.bp,ctx.id,s=>{addBreakdownActivity(s,"analyst",`Wrote ${made} ${made===1?"story":"stories"} for ${epic.title}`),s.job&&(s.job.found=counts(after))})}let b=updateBreakdown(ctx.bp,ctx.id,x=>{withFirstStory(ctx.bp,x),x.status="ready",delete x.error}),c=counts(b);return updateBreakdownState(ctx.bp,ctx.id,s=>{s.job={step:"draft",status:"done",found:c},addBreakdownActivity(s,"analyst",`The breakdown is ready for you: ${c.epics} epics, ${c.features} features and ${c.stories} stories`)}),!1}function keyFor(m,raw,level){let k=String(raw??"").trim().toUpperCase(),prefix={epic:"E-",feature:"F-",story:"S-"}[level];if(k.startsWith(prefix)&&m.own.has(k)&&!m.used.has(k))return m.used.add(k),m.map.set(k,k),k;let fresh=nextKey(m.b,level);return m.used.add(fresh),k&&m.map.set(k,fresh),fresh}function hasStarted(bp,s){let t=s.ticket?readTicket(bp,s.ticket):null;return!!(t&&!t.deleting)}function startedIn(bp,stories){return new Map(stories.filter(s=>hasStarted(bp,s)).map(s=>[s.key,structuredClone(s)]))}function andWords3(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}function mergeStories(m,outs,started){return(outs??[]).map(o=>{let k=keyFor(m,o.key,"story"),kept=started.get(k);if(kept)return started.delete(k),kept;let s=story(o,k);return m.made.push({s,needs:o.needs??[]}),s})}function mergeFeature(m,o,started){return{key:keyFor(m,o.key,"feature"),title:String(o.title??"").trim()||"Untitled feature",...o.summary?.trim()?{summary:o.summary.trim()}:{},...cleanRef(o.ref)?{ref:cleanRef(o.ref)}:{},stories:mergeStories(m,o.stories,started)}}function mergeEpic(m,o,started){return{key:keyFor(m,o.key,"epic"),title:String(o.title??"").trim()||"Untitled epic",...o.goal?.trim()?{goal:o.goal.trim()}:{},...cleanRef(o.ref)?{ref:cleanRef(o.ref)}:{},features:(o.features??[]).map(f=>mergeFeature(m,f,started)),stories:mergeStories(m,o.stories,started)}}function keepLinks(before,after){let old=new Map;for(let e of before.epics){old.set(e.key,{tracker:e.tracker});for(let f of e.features)old.set(f.key,{tracker:f.tracker})}for(let{story:s}of allStories(before))old.set(s.key,{tracker:s.tracker,ticket:s.ticket});let keep=x=>{let o=old.get(x.key);o?.tracker&&!x.tracker&&(x.tracker=o.tracker)};for(let e of after.epics){keep(e);for(let f of e.features)keep(f)}for(let{story:s}of allStories(after)){keep(s);let o=old.get(s.key);o?.ticket&&!s.ticket&&(s.ticket=o.ticket)}}function startedWords(s){return`${s.key} started as ${s.ticket}: change it there.`}async function redoStep(ctx,b,job){let whole=job.item===b.id,target=whole?null:findItem(b,job.item??"");if(!whole&&!target)throw new StepError(`${job.item} is no longer in the breakdown.`);if(target?.story&&hasStarted(ctx.bp,target.story))throw new StepError(startedWords(target.story));let session=readBreakdownState(ctx.bp,ctx.id).sessions?.read,level=target?.level,schema=whole?WHOLE_REDO_SCHEMA:level==="epic"?EPIC_REDO_SCHEMA:level==="feature"?FEATURE_REDO_SCHEMA:STORY_REDO_SCHEMA;setNow(ctx,`Redoing ${whole?"the breakdown":target.item.key}`);let out18=(await ask(ctx,{step:"spec",prompt:redoPrompt(ctx.project,whole?withoutFirstStory(b):b,target?.item??null,job.ask??"",documentFiles(ctx.bp,b),!!session),schema,...session?{resume:session}:{},timeoutMs:BRD_WRITE_MS})).structured,cut=0,kept=[],rewritten=[];return updateBreakdown(ctx.bp,ctx.id,x=>{let before=structuredClone(x),t=whole?null:findItem(x,job.item);if(!whole&&!t)return;if(t?.story&&hasStarted(ctx.bp,t.story))throw new StepError(startedWords(t.story));let inside5=t?[t.item.key,...storiesIn(t).map(s=>s.key),...t.level==="epic"?t.epic.features.map(f=>f.key):[]]:allKeys(x),m={b:x,own:new Set(inside5),map:new Map,used:new Set,made:[]},started=startedIn(ctx.bp,t?storiesIn(t):allStories(x).map(p=>p.story));if(kept=[...started.values()],whole)x.epics=(out18.epics??[]).map(e=>mergeEpic(m,e,started)),out18.leftOut&&(x.leftOut=out18.leftOut.map(l=>({text:String(l.text??"").trim(),why:String(l.why??"").trim(),...cleanRef(l.ref)?{ref:cleanRef(l.ref)}:{}})).filter(l=>l.text)),out18.summary?.trim()&&(x.summary=out18.summary.trim()),started.size&&x.epics[0]&&x.epics[0].stories.push(...started.values());else if(t.level==="epic"&&out18.epic){let e=mergeEpic(m,{...out18.epic,key:t.item.key},started);e.stories.push(...started.values()),x.epics.splice(x.epics.indexOf(t.epic),1,e)}else if(t.level==="feature"&&out18.feature){let f=mergeFeature(m,{...out18.feature,key:t.item.key},started);f.stories.push(...started.values()),t.epic.features.splice(t.epic.features.indexOf(t.feature),1,f)}else if(t.level==="story"&&out18.story){let s=story({...out18.story},t.item.key);m.made.push({s,needs:out18.story.needs??[]}),t.list.splice(t.list.indexOf(t.item),1,s)}keepLinks(before,x),resolveNeeds(x,m.made,m.map);let left=new Set(allKeys(x));for(let{story:s}of allStories(x))s.dependsOn=s.dependsOn.filter(n=>left.has(n));let redone=t&&left.has(t.item.key)?t.item.key:void 0;x.decided=x.decided.flatMap(d=>{if(!d.item||left.has(d.item))return[d];if(!d.changed)return[];let{item:_gone,...rest}=d;return[redone?{...rest,item:redone}:rest]}),cut=cutLoops(x),addDecided2(x,out18.decided,m.map,()=>"analyst"),withFirstStory(ctx.bp,x),x.status==="approved"&&(rewritten=rewrittenStories(before,x))}),updateBreakdownState(ctx.bp,ctx.id,s=>{if(s.job={step:"redo",status:"done",item:job.item,...job.ask?{ask:job.ask}:{}},rewritten.length&&(s.rewritten=[...new Set([...s.rewritten??[],...rewritten])]),addBreakdownActivity(s,"analyst",`Redid ${whole?"the breakdown":job.item}${cut?`, and took away ${cut} ${cut===1?"need":"needs"} that went round in a loop`:""}`),kept.length){let one2=kept.length===1;addBreakdownActivity(s,"squad",`Kept ${andWords3(kept.map(k=>k.key))} as ${one2?"it is":"they are"}: ${one2?"it":"they"} started as ${andWords3(kept.map(k=>k.ticket))}. Change ${one2?"it":"them"} there`)}}),!1}var STEPS4={text:textStep,read:readStep,reply:replyStep,draft:draftStep,redo:redoStep};function failed(ctx,job,err2){let{bp,id:id2}=ctx;if(err2 instanceof LimitReached||err2 instanceof HeldAtBudget){updateBreakdownState(bp,id2,s=>{!s.job||s.job.status==="stopping"||(s.job.status="pending",s.job.now=err2 instanceof LimitReached?limitText(err2.pause):err2.message,err2 instanceof HeldAtBudget&&(s.job.heldFor="budget"))});return}if(readBreakdownState(bp,id2).job?.status==="stopping"){breakdownStopped(bp,id2);return}if(ctx.deps.signal?.aborted||err2 instanceof Stopped)return;let capped=err2 instanceof CapReached,own2=ownWords2(err2)??(err2 instanceof DocumentError||capped?err2.message:null),{what,detail}=explainError(err2);writeLog({level:own2?"warn":"error",...ctx.project?{project:ctx.project}:{},text:`${id2}: the Analyst's ${job.step} step stopped: ${[what,detail].filter(Boolean).join(`
1706
1706
  `)}`});let plain=`${own2??TECHNICAL_STOP}${job.step==="read"&&!capped&&!(err2 instanceof DocumentError)?READ_AGAIN:""}`;job.step==="reply"?updateBreakdown(bp,id2,x=>{let q4=x.questions.find(y=>y.id===job.question);q4&&(q4.thinking=!1,q4.thread=[...q4.thread??[],{who:"squad",text:own2?`Could not answer: ${own2}`:NO_ANSWER,at:now()}])}):job.step!=="redo"&&updateBreakdown(bp,id2,x=>{x.status="failed",x.error=plain}),updateBreakdownState(bp,id2,s=>{s.job&&(s.job.status="failed",job.step==="reply"&&!own2?delete s.job.error:s.job.error=plain,delete s.job.now,capped&&(s.job.capped=!0)),addBreakdownActivity(s,"analyst",job.step==="reply"?own2?`Could not answer your question: ${own2}`:"Could not answer your question this time":`${job.step==="redo"?`Could not redo ${job.item}`:"Stopped"}: ${plain}`)})}async function runBreakdownJob(bp,id2,deps){let ctx={bp,id:id2,deps,project:readMeta(bp).project},mark=sleepMark();for(let steps=0;steps<6;steps+=1){let job=readBreakdownState(bp,id2).job,b=readBreakdown(bp,id2);if(!b||!job||job.status!=="pending"&&job.status!=="running")return;updateJob(bp,id2,j=>{j.status="running",j.startedAt??=now(),delete j.error,delete j.heldFor});try{if(!await STEPS4[job.step](ctx,b,job))return}catch(err2){if(sleptSince(mark)&&!(err2 instanceof LimitReached)&&!(err2 instanceof HeldAtBudget)&&!(err2 instanceof Stopped)&&!(err2 instanceof CapReached)){updateBreakdownState(bp,id2,s=>{!s.job||s.job.status==="stopping"||(s.job.status="pending",s.job.now="The computer slept, so this step starts again")});return}failed(ctx,job,err2);return}}}function who3(bp){return readMeta(bp).user.name}function and3(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}var usd2=n=>`$${n.toFixed(2)}`;function ticketsOf2(bp,stories){let out18=new Map;for(let{story:story2}of stories){let t=story2.ticket?readableTicket(bp,story2.ticket):null;t&&!t.deleting&&out18.set(story2.key,t)}return out18}function deletedWords(keys){return keys.length===1?`The ticket of ${keys[0]} was deleted, so it does not start by itself. Press Start on it to build it again.`:`The tickets of ${and3(keys)} were deleted, so they do not start by themselves. Press Start on one to build it again.`}function working(t){return!!(t&&!isMerged(t)&&!t.waiting&&!t.archived&&!mergeWait(t))}function setBuild(bp,id2,on){let b=requireBreakdown(bp,id2);if(!isBreakdownMine(b,readMeta(bp)))throw new BoardActionError(`${b.id} is ${ownerOfBreakdown(b)}'s. Only their squad builds it.`);let was=readBreakdownState(bp,b.id).build,by2=who3(bp);if(!on)return was?.on&&updateBreakdownState(bp,b.id,s=>{s.build={on:!1,by:by2,at:now(),...was.heldFor==="budget"?{heldFor:"budget"}:{},...was.budgetOk?{budgetOk:was.budgetOk}:{}},addBreakdownActivity(s,"you","Paused Build continuously. The stories building now carry on; no new one starts")}),b;if(b.status!=="approved"||!b.approved)throw new BoardActionError(`Approve ${b.id} first. Nothing is built before the breakdown is approved.`);let stories=allStories(b),tickets=ticketsOf2(bp,stories);if(stories.every(({story:story2})=>tickets.get(story2.key)&&isMerged(tickets.get(story2.key))))throw new BoardActionError(`Every story of ${b.id} is merged. Nothing is left to build.`);let atBudget=was?.heldFor==="budget",unread=unreadStories(b,readBreakdownState(bp,b.id));if(was?.on&&!atBudget&&!unread.length)return b;let month=monthKey();return updateBreakdownState(bp,b.id,s=>{s.build={on:!0,by:by2,at:was?.on?was.at:now(),...atBudget?{budgetOk:month}:was?.budgetOk===month?{budgetOk:month}:{}},delete s.rewritten,unread.length&&was?.on?addBreakdownActivity(s,"you",`Let the rewritten ${unread.length===1?"story":"stories"} build: ${and3(unread)}`):atBudget?addBreakdownActivity(s,"you","Let it go on past this month's budget"):addBreakdownActivity(s,"you",`Turned on Build continuously, ${parallelLimit(readMeta(bp).parallel)} at a time`)}),b}function placesLeft(limit,tickets,meta,othersRunning,othersWaiting){if(othersWaiting)return 0;let going=tickets.filter(t=>t.squad&&!t.waiting&&!t.deleting&&!t.archived&&isMine(t,meta)).length;return limit-othersRunning-going}function buildingBreakdowns(bp,meta=readMeta(bp)){return listBreakdowns(bp).filter(b=>b.status==="approved"&&isBreakdownMine(b,meta)&&readBreakdownState(bp,b.id).build?.on)}async function fillBuildQueue(bp,room){let meta=readMeta(bp),turns=[];for(let b of buildingBreakdowns(bp,meta))turns.push(await queueTurn(bp,b,meta,room));return turns}function waitsWords(stories,tickets,deleted){let merged2=k=>!!(tickets.get(k)&&isMerged(tickets.get(k))),left=stories.filter(({story:story2})=>!tickets.has(story2.key)&&!deleted.includes(story2.key)),youWords=keys=>{let you=keys.filter(k=>tickets.get(k)?.waiting),pr=keys.filter(k=>mergeWait(tickets.get(k))==="pr"),copy2=keys.filter(k=>mergeWait(tickets.get(k))==="checkout"),one2=(xs,a,many2)=>xs.length===1?a:many2;return[you.length?` ${and3(you)} ${one2(you,"needs","need")} you.`:"",pr.length?` ${and3(pr)} ${one2(pr,"waits for someone to approve its pull request","wait for someone to approve their pull requests")}.`:"",copy2.length?` ${and3(copy2)} ${one2(copy2,"waits","wait")} to merge: your own copy of the code has changes in the same files.`:""].join("")};if(!left.length){let you=youWords(stories.map(({story:story2})=>story2.key).filter(k=>!merged2(k)));return deleted.length?you.trim():you?`Every story has started.${you}`:"Every story has started. The build is done when the last ones merge."}let byKey=new Map(stories.map(({story:story2})=>[story2.key,story2])),roots=new Set,seen=new Set,visit=k=>{if(!seen.has(k)){seen.add(k);for(let n of byKey.get(k)?.dependsOn??[])!byKey.has(n)||merged2(n)||deleted.includes(n)||(tickets.has(n)?roots.add(n):visit(n))}};for(let{story:story2}of left)visit(story2.key);let order=stories.map(({story:story2})=>story2.key).filter(k=>roots.has(k));if(order.length)return`The next stories wait for ${and3(order)} to merge.${youWords(order)}`;let bare=left.filter(({story:story2})=>!story2.criteria.length).map(({story:story2})=>story2.key);return bare.length?`${and3(bare)} ${bare.length===1?"has":"have"} no "done when" yet. Add one and ${bare.length===1?"it starts":"they start"}.`:deleted.length?"":"Nothing more can start now."}async function budgetHold(bp,meta,state,room){if(!meta.budget?.monthUsd)return{goOn:!1};let b=budgetState(bp,null,meta.budget);return b.over.includes("month")?state.build?.budgetOk===monthKey()?{goOn:!0}:await room.payment()!=="api-key"?{goOn:!1}:{held:`This month's budget is used up: ${usd2(b.monthSpent)} of ${usd2(b.monthUsd??0)}. No new story starts. Raise the budget in Settings, or press Build continuously again to go on this month.`,goOn:!1}:{goOn:!1}}function analystOverBudget(bp,id2){let budget=readMeta(bp).budget;if(!budget?.monthUsd)return null;let b=budgetState(bp,null,budget);if(!b.over.includes("month"))return null;let s=readBreakdownState(bp,id2),month=monthKey();return s.budgetOk===month||s.build?.budgetOk===month?null:`This month's budget is used up: ${usd2(b.monthSpent)} of ${usd2(b.monthUsd??0)}. The Analyst waits. Raise the budget in Settings, or press Carry on to go on this month`}function troubleWords(stories,tickets){let stops=stories.map(({story:story2})=>story2.key).flatMap(k=>{let t=tickets.get(k),stop=t&&!isMerged(t)&&t.waiting?stopOf(t):void 0;return stop==="other"||stop==="check"?[{k,stop}]:[]});if(stops.length<2)return null;let names2=and3(stops.map(s=>s.k));return stops.every(s=>s.stop==="check")?`${names2} stopped on a failed check or an app that would not open. No new story starts until you settle one of them, so the next ones do not pay to reach the same stop.`:`${names2} stopped on a problem. No new story starts until you settle one of them.`}async function queueTurn(bp,b,meta,room){let state=readBreakdownState(bp,b.id),stories=allStories(b),tickets=ticketsOf2(bp,stories),merged2=k=>!!(tickets.get(k)&&isMerged(tickets.get(k)));if(stories.length&&stories.every(({story:story2})=>merged2(story2.key)))return updateBreakdownState(bp,b.id,s=>{s.build?.on&&(s.build={on:!1,by:s.build.by,at:now()},addBreakdownActivity(s,"squad",stories.length===1?"Its story is merged. Send the release out from Releases.":`All ${stories.length} stories are merged. Send the release out from Releases.`))}),{id:b.id,started:[],done:!0};let keys=new Set(stories.map(({story:story2})=>story2.key)),missing2=stories.filter(({story:story2})=>story2.ticket&&!tickets.has(story2.key)),unreadable=missing2.filter(({story:story2})=>ticketProblem(bp,story2.ticket)).map(({story:story2})=>story2.ticket),deleted=missing2.filter(({story:story2})=>!unreadable.includes(story2.ticket)).map(({story:story2})=>story2.key),able=stories.filter(({story:story2})=>!story2.ticket&&story2.criteria.length>0&&story2.dependsOn.every(n=>!keys.has(n)||merged2(n))),redo=able.map(({story:story2})=>redoHolding(b,state.job,story2.key)).find(Boolean),ready=able.filter(({story:story2})=>!redoHolding(b,state.job,story2.key)),unread=unreadStories(b,state),held4=null,started=[],goOn3=!1;if(unreadable.length)held4={text:`The file of ${and3(unreadable)} could not be read, so no new story starts. An engineer can look at it.`,for:"waits"};else if(unread.length)held4={text:`The Analyst rewrote ${and3(unread)} after you approved. No new story starts until you read ${unread.length===1?"it":"them"} and press Build continuously again.`,for:"review"};else if(!ready.length&&redo)held4={text:`The Analyst is redoing ${redo}. Its stories start once the redo is done.`,for:"waits"};else if(!ready.length)held4={text:[waitsWords(stories,tickets,deleted),deleted.length?deletedWords(deleted):""].filter(Boolean).join(" "),for:"waits"};else if(room.pause)held4={text:limitText(room.pause),for:"limit"};else{let budget=await budgetHold(bp,meta,state,room),trouble=troubleWords(stories,tickets);goOn3=budget.goOn,budget.held?held4={text:budget.held,for:"budget"}:trouble&&(held4={text:trouble,for:"waits"})}if(!held4){let busy=stories.filter(({story:story2})=>working(tickets.get(story2.key))).length,places=Math.min(Math.min(parallelLimit(meta.parallel),meta.atOnce??1/0)-busy,room.free());for(let{story:story2}of ready.slice(0,Math.max(0,places)))try{let t=startStory(bp,b.id,story2.key,{by:"squad"});goOn3&&updateTicket(bp,t.id,x=>x.budgetOk={...x.budgetOk,month:monthKey()}),started.push(t.id)}catch(err2){held4={text:`${story2.key} could not start: ${plainWords(err2,`Starting ${story2.key} of ${b.id}`,"something technical stopped it. An engineer can see why in the log.")}`,for:"waits"};break}!held4&&deleted.length&&(held4={text:deletedWords(deleted),for:"waits"})}let before=state.build;return before&&(before.held!==held4?.text||before.heldFor!==held4?.for)&&updateBreakdownState(bp,b.id,s=>{if(!s.build?.on)return;let{held:_h,heldFor:_f,...rest}=s.build;s.build={...rest,...held4?{held:held4.text,heldFor:held4.for}:{}}}),{id:b.id,started,...held4?{held:held4.text}:{}}}var NO_TRACKER="This project has no tracker squad-kit can make items in. Connect one in Settings, then send.";function plural3(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function parts(b){return b.epics.flatMap(e=>[e,...e.features,...e.features.flatMap(f=>f.stories),...e.stories])}function firstSend(b,module){return!parts(b).some(p=>p.tracker?.module===module)}async function sendPreview(bp,id2){let b=requireBreakdown(bp,id2),tracker2=sendTracker(bp);if(!tracker2)throw new BoardActionError(NO_TRACKER);let plan=planSend(b,tracker2.name),levels;try{levels=await tracker2.levels()}catch(err2){throw new BoardActionError(`squad-kit could not read the item types ${tracker2.label} uses: ${plainWords(err2,"Reading item types","something technical stopped it. Try again.")}`)}let project=levels.project??sendTargetOf(bp)?.project;return{tracker:{module:tracker2.name,label:tracker2.label,...project?{project}:{}},levels:{epic:levels.epic,feature:levels.feature,story:levels.story,process:levels.process},counts:plan.counts,items:plan.items.length,links:linkCount(plan),first:firstSend(b,tracker2.name)}}function sending(s){return s.sync?.status==="pending"||s.sync?.status==="running"}function requestSend(bp,id2){let b=requireBreakdown(bp,id2);if(!isBreakdownMine(b,readMeta(bp)))throw new BoardActionError(`${b.id} is ${ownerOfBreakdown(b)}'s. Only their squad sends it.`);let tracker2=sendTracker(bp);if(!tracker2)throw new BoardActionError(NO_TRACKER);if(b.status!=="approved"||!b.approved)throw new BoardActionError(`Approve ${b.id} first. Nothing is sent to ${tracker2.label} before the breakdown is approved.`);let state=readBreakdownState(bp,b.id);if(sending(state))throw new BoardActionError(`${b.id} is being sent to ${tracker2.label} now: ${state.sync.done} of ${state.sync.of} done.`);let plan=planSend(b,tracker2.name),links=linkCount(plan),of=plan.items.length+links;if(!of)throw new BoardActionError(`${tracker2.label} has ${b.id} as it is now. Nothing changed since the last send.`);let first2=firstSend(b,tracker2.name),what=[plan.items.length?plural3(plan.items.length,"item"):"",links?plural3(links,"link"):""].filter(Boolean).join(" and ");return updateBreakdownState(bp,b.id,s=>{let making2=s.sync?.making;s.sync={status:"pending",done:0,of,items:{done:0,of:plan.items.length},links:{done:0,of:links},...making2?{making:making2}:{},by:readMeta(bp).user.name,at:now()},addBreakdownActivity(s,"you",first2?`Sending ${b.id} to ${tracker2.label}: ${what}`:`Sending the changes to ${tracker2.label}: ${what}`)}),b}function sentItems(bp,module){return listBreakdowns(bp).flatMap(parts).map(p=>p.tracker).filter(l=>l?.module===module).map(l=>({id:l.id,type:l.type,title:l.ref}))}function doneWords(r,label3,tickets){let what=[r.created?`${r.created} new`:"",r.updated?`${r.updated} changed`:""].filter(Boolean).join(" and "),links=r.linked?`${plural3(r.linked,"link")} between stories that need each other`:"",lead=[what?`${what} ${r.created+r.updated===1?"item":"items"}`:"",links].filter(Boolean).join(", ")||"nothing new";return`Sent to ${label3}: ${lead}${tickets.length?`. ${tickets.join(", ")} now move${tickets.length===1?"s":""} forward there as ${tickets.length===1?"it builds":"they build"}`:""}`}async function runSend(bp,id2,opts){let state=readBreakdownState(bp,id2);if(!sending(state))return null;let b=readBreakdown(bp,id2),tracker2=opts.tracker,fail=error2=>updateBreakdownState(bp,id2,s=>{s.sync&&(s.sync={...s.sync,status:"failed",error:error2}),addBreakdownActivity(s,"squad",error2)});if(!b)return null;tracker2 instanceof DemoTracker&&tracker2.remember(sentItems(bp,tracker2.name));let before=state.sync.done,was={items:state.sync.items?.done??0,links:state.sync.links?.done??0};updateBreakdownState(bp,id2,s=>{s.sync&&(s.sync.status="running",delete s.sync.error)});let result;try{result=await sendBreakdown(b,tracker2,{save:(_b,change)=>updateBreakdown(bp,id2,x=>applySendChange(x,change)),progress:(done,of,p)=>updateBreakdownState(bp,id2,s=>{s.sync&&(s.sync={...s.sync,done:before+done,of:before+of,items:{done:was.items+p.items.done,of:was.items+p.items.of},links:{done:was.links+p.links.done,of:was.links+p.links.of}})}),making:key=>updateBreakdownState(bp,id2,s=>{s.sync&&(key?s.sync.making=key:delete s.sync.making)}),...state.sync.making?{cutOff:state.sync.making}:{},...opts.signal?{signal:opts.signal}:{},by:state.sync.by??b.owner})}catch(err2){return fail(err2.message),null}if(result.stopped)return updateBreakdownState(bp,id2,s=>s.sync&&(s.sync.status="pending")),result;let linked=linkTickets(bp,requireBreakdown(bp,id2));return result.error?(fail(result.error),result):(updateBreakdownState(bp,id2,s=>{s.sync={status:"done",done:before+result.done,of:before+result.of,...s.sync?.items?{items:s.sync.items}:{},...s.sync?.links?{links:s.sync.links}:{},...s.sync?.by?{by:s.sync.by}:{},at:now()},addBreakdownActivity(s,"squad",doneWords(result,tracker2.label,linked))}),result)}function sendsDue(bp,meta=readMeta(bp)){return listBreakdowns(bp).filter(b=>isBreakdownMine(b,meta)&&sending(readBreakdownState(bp,b.id))).map(b=>b.id)}function startSends(bp,meta,jobs){for(let id2 of sendsDue(bp,meta)){let key=`__send:${id2}`;if(jobs.running.has(key))continue;let tracker2=sendTracker(bp);if(!tracker2){updateBreakdownState(bp,id2,s=>s.sync&&Object.assign(s.sync,{status:"failed",error:NO_TRACKER}));continue}let stop=new AbortController;jobs.running.add(key),jobs.stops.set(key,stop),runSend(bp,id2,{tracker:tracker2,signal:stop.signal}).catch(()=>{}).finally(()=>{jobs.running.delete(key),jobs.stops.delete(key)})}}var demoJobs=new Map;function demoSendJobs(bp){let j=demoJobs.get(bp.root);return j||(j={running:new Set,stops:new Map},demoJobs.set(bp.root,j)),j}var MAX_CHECK_TRIES=3,PROJECT_CHECKS=["test","lint","build","check","start"],MAX_REVIEW_ROUNDS=2,WORDS_STEP_MS=10*6e4,ANSWER_STEP_MS=5*6e4,BUILD_STEP_MS=60*6e4,REVIEW_STEP_MS=15*6e4,PROOF_STEP_MS=30*6e4,PICK_UP_MS=2*6e4,BEFORE_SHOTS_MS=8*6e4,STOPPED10="You stopped the squad.",BUILDER_PHASES=new Set(["build","unstick","fix","answered-builder","chat-carry-builder","review-fix","restore-tests","update-base","change-build"]),KEPT_APP_MS=10*6e4,CHECKS_WAITED="The checks waited too long for the app starting in this story's copy, so they did not run. Press Try again to run them.",APP_WAITED="squad-kit could not start the app: the tests in this story's copy were still running after a long wait. Press Try again to try once more.",CHECKS_PASSED_FOR_BUILDER="The checks already passed on this same code when squad-kit ran them for the Builder, so they do not run again",DB_TROUBLE=/database|migrat|schema|drift|sqlite|prisma|alembic|sequelize|knex|typeorm|flyway|liquibase|entity ?framework/i,STEP_NUMBER=/^\s*(?:step\s*(\d+)\b|\(?(\d+)\s*(?:[.):]|\s[-–])(?=\s|$))/i;function planStepOf(entries,count3){if(!count3)return null;let numbered=entries.flatMap(e=>{let m=STEP_NUMBER.exec(e.content),n=m?Number(m[1]??m[2]):NaN;return n>=1&&n<=count3?[{n:n-1,status:e.status}]:[]}).sort((a,b)=>a.n-b.n),list2=numbered.length?numbered:entries.length===count3?entries.map((e,i)=>({n:i,status:e.status})):[];if(!list2.length)return null;let doing=list2.find(e=>e.status==="in_progress"),open3=list2.find(e=>e.status!=="completed");return doing?doing.n:open3?open3.n:Math.min(count3-1,list2[list2.length-1].n+1)}function someFromBuilder(names2){let words2=names2.map(n=>n==="test"?"the tests":n==="lint"?"the code check":n==="build"?"the build":`the ${n} check`),what=words2.length>1?`${words2.slice(0,-1).join(", ")} and ${words2[words2.length-1]}`:words2[0],many2=words2.length>1||names2[0]==="test";return`${what.charAt(0).toUpperCase()}${what.slice(1)} already passed on this same code when squad-kit ran ${many2?"them":"it"} for the Builder, so ${many2?"they do":"it does"} not run again. squad-kit is running the other checks`}function runChecks2(...a){let signal2=a[3]?.signal,work=async()=>(await stopAppsIn(a[0]),runChecks(...a));return inTurn2(a[0],"checks",work,signal2?{signal:signal2}:{}).catch(turnStop)}function runGatesAt2(...a){let dirs=a[2].map(p=>p.dir);return inTurn2(dirs,"checks",async()=>(await stopAppsIn(dirs),runGatesAt(...a))).catch(turnStop)}function turnStop(err2){throw err2 instanceof TurnTimeout?new StepError(CHECKS_WAITED,`The checks ${err2.message}.`):err2}async function answersAt(url){return fetch(url,{redirect:"manual",signal:AbortSignal.timeout(3e3)}).then(()=>!0,()=>!1)}function projectRoot(bp){return bp.root}function planKey(t){return createHash11("sha1").update(JSON.stringify(planSnapshot(t))).digest("hex").slice(0,12)}async function secretFiles(bp,cwd){return[bp.secretsFile,path116.join(bp.squadDir,"secrets.yaml"),...await hiddenFiles(bp,cwd)]}var squads=new Set,waiting=new Set;function ticketsRunning(){let n=0;for(let s of squads)n+=s.ticketsRunning();return n}var holding=!1;function holdNewRuns(on){holding=on}var limitSeen=null;function agentRuns(){return[...squads].map(s=>s.runs()).filter(r=>r.keys.length>0)}function queuedText(running3,limit){return queuedLine(running3.length,limit)}var PassingTrouble=class extends StepError{},BLIP_WAITS_MIN=[1,2,4,8],TURNS_BEFORE_BANNER=3,LiveSquad=class{constructor(bp){this.bp=bp}bp;running=new Set;timer=null;stops=new Map;lanesOf=new Map;syncing=new Set;paymentLooks=new Set;localRuns=new Set;mergeLooks=new Map;closing=!1;turnsFailed=0;dog=new Watchdog;lastWatch=0;lastPickUp=0;pickingUp=!1;queueing=!1;beforeQueue=new Map;listNoes=new Map;beforeShotsOf=new Map;swept=null;marks=new Map;kept=new Map;builderRuns=new Map;start(intervalMs=1e3){return this.resumeAfterRestart(),lookOutsideSoon(this.bp),squads.add(this),this.timer=setInterval(()=>this.safeTick(),intervalMs),()=>{forgetSaveTrouble(this.bp),squads.delete(this),waiting.delete(this),this.closing=!0,this.timer&&clearInterval(this.timer);for(let id2 of this.ticketIds())this.cutOff(id2,"restart");for(let c of this.stops.values())c.abort();for(let id2 of[...this.kept.keys()])this.dropKept(id2);endSaves(this.bp),this.isLive()&&trackTask(this.clearAfterClose())}}isLive(){try{return boardExists(this.bp)&&readMeta(this.bp).engine==="live"}catch{return!1}}cutOff(id2,by2){try{this.update(id2,x=>{x.squad&&(x.squad.phase=markInterrupted(x,x.squad.phase,by2),pauseClock(x))})}catch{}}async clearAfterClose(){await runsEnded(projectCopies(this.bp),6e3),await this.clearCopies(projectCopies(this.bp),{busy:this.busyCopies(void 0,!1),graceMs:2500}).catch(()=>{})}ticketsRunning(){return this.ticketIds().length}ticketIds(){return[...this.running].filter(k=>!k.startsWith("__"))}runs(){return{root:this.bp.root,keys:[...this.running]}}mayStart(limit){let mine2=this.ticketsRunning(),own2=this.meta().atOnce;return own2&&mine2>=own2?!1:ticketsRunning()+(squads.has(this)?0:mine2)<limit&&![...waiting].some(s=>s!==this&&s.ticketsRunning()<mine2)?(waiting.delete(this),!0):(waiting.add(this),!1)}mendCopy(t){if(!t.worktree||t.stage==="done"||!fs124.existsSync(t.worktree))return!1;for(let c of t.copies??[])fs124.existsSync(c.dir)&&(git(c.dir,["rev-parse","--git-dir"],{allowFail:!0})?settleCopy(c.dir):repairCopyLink(path116.join(projectRoot(this.bp),c.repo),c.dir));let repo=path116.join(projectRoot(this.bp),repoFor(readMeta(this.bp),t).path);if(git(t.worktree,["rev-parse","--git-dir"],{allowFail:!0})){let onDisk=settleCopy(t.worktree);return onDisk&&this.update(t.id,x=>addActivity(x,"squad",cutOffWords(onDisk))),!1}let mended=repairCopyLink(repo,t.worktree);return this.log(mended?"info":"warn",mended?`The project's folder moved; reconnected the copy at ${t.worktree}`:`The copy at ${t.worktree} has lost its link to the repo, and git could not repair it`,t.id),mended&&this.update(t.id,x=>addActivity(x,"squad","The project's folder moved, so squad-kit reconnected this ticket's copy of the code")),mended}resumeAfterRestart(){try{if(!boardExists(this.bp)||readMeta(this.bp).engine!=="live")return;resumeCutRelease(this.bp).then(r=>r&&this.log("warn",`Settled ${r.version}, which a restart cut off: ${r.status==="released"?"finished":"undone"}`)),resumeSetup(this.bp);for(let t of listTickets(this.bp))if(this.mendCopy(t),t.squad&&Date.parse(t.squad.nextAt)>Date.now()){let cut=!t.interrupted&&Date.parse(t.squad.nextAt)-Date.now()>6e5;this.update(t.id,x=>{cut&&x.squad&&(x.squad.phase=markInterrupted(x,x.squad.phase,"restart")),x.squad&&(x.squad.nextAt=now()),resumeClock(x)})}this.swept=this.clearCopies(projectCopies(this.bp),{busy:this.busyCopies()}).catch(()=>{}),rewireStoryDbs(this.bp),tidyStoryDbs(this.bp).catch(()=>{});for(let repo of reposOf(readMeta(this.bp)))clearHalfCopies(path116.join(projectRoot(this.bp),repo.path)).catch(()=>{})}catch{}}safeTick(){try{this.tick(),this.turnsFailed=0,endTurnTrouble(this.bp)}catch(err2){if(this.holdForSaving(err2))return;this.turnsFailed+=1,(this.turnsFailed===1||this.turnsFailed===TURNS_BEFORE_BANNER)&&this.log("error",`A turn of the squad failed: ${err2.stack??err2.message}`),this.turnsFailed>=TURNS_BEFORE_BANNER&&noteTurnTrouble(this.bp,err2)}}holdForSaving(err2){return isSaveTrouble(this.bp,err2)?(noteSaveTrouble(this.bp,err2)&&this.log("error",`squad-kit cannot save its files for this project, so its squad waits: ${err2.message}`),!0):!1}savingHeld(){let{held:held4,fixed}=stillHeld(this.bp);return fixed&&this.carryOnAfterTrouble(),held4}carryOnAfterTrouble(){this.log("info","squad-kit can save its files again, so the squad carries on");for(let t of listTickets(this.bp))t.squad&&!this.running.has(t.id)&&Date.parse(t.squad.nextAt)>Date.now()&&this.update(t.id,x=>{x.squad&&(x.squad.nextAt=now())})}tick(){if(waiting.delete(this),this.savingHeld()||!boardExists(this.bp))return;let meta;try{meta=readMeta(this.bp)}catch{return}if(meta.engine!=="live")return;this.watch(),this.sweepKept(),this.pickUp(),lookOutsideBeside(this.bp),tidyStoryDbsBeside(this.bp);let limit=parallelLimit(meta.parallel),pause2=activePause();if(pause2?limitSeen=pause2.until:limitSeen&&(limitSeen=null,this.log("info","The usage limit wait is over: the queue carries on")),meta.setup?.stopping&&(this.stops.get("__scan")?.abort(),this.stops.get("__setup")?.abort(),this.stops.get("__firstSave")?.abort(),!this.running.has("__scan")&&!this.running.has("__setup")&&!this.running.has("__firstSave"))){settleSetupStop(this.bp);return}if(firstSaveHolds(meta)){firstSaveDue(meta)&&!meta.setup?.stopping&&!this.running.has("__firstSave")&&this.launch("__firstSave",()=>runFirstSave(this.bp,this.stops.get("__firstSave").signal));return}if(this.talk(),meta.scan?.status==="pending"&&!this.running.has("__scan")){this.launch("__scan",()=>this.scan());return}if(meta.scan?.status==="done"&&nextProofStep(meta.setup)&&!this.running.has("__setup")&&this.launch("__setup",()=>runSetupProof(this.bp,{shoot:o=>this.setupShot(o),signal:this.stops.get("__setup").signal})),meta.scan?.status==="running"&&!this.running.has("__scan")){this.launch("__scan",()=>this.scan());return}(meta.blueprint?.status==="pending"||meta.blueprint?.status==="running")&&!this.running.has("__blueprint")&&this.launch("__blueprint",()=>this.readBlueprint());for(let c of meta.impactChecks??[]){let key=`__impact:${c.adr}`;(c.status==="pending"||c.status==="running")&&!this.running.has(key)&&this.launch(key,()=>this.checkImpact(c.adr))}for(let d of meta.techDrafts??[]){let key=`__tech:${d.tech}`;d.status==="writing"&&!this.running.has(key)&&this.launch(key,()=>this.writeTechDecision(d.tech))}for(let j of breakdownJobs(this.bp,meta)){let key=`__breakdown:${j.id}`;if(j.job.status==="stopping")this.stops.get(key)?.abort(),this.running.has(key)||breakdownStopped(this.bp,j.id);else{if(this.running.has(key))continue;if(j.job.heldFor==="long")continue;pause2?holdBreakdownJob(this.bp,j.id,limitText(pause2)):j.job.heldFor==="budget"&&analystOverBudget(this.bp,j.id)?this.letGoOnSubscription(j.id):this.launch(key,()=>this.breakdownJob(j.id))}}startSends(this.bp,meta,{running:this.running,stops:this.stops});for(let t of listTickets(this.bp)){if(t.deleting){this.letGo(t);continue}trackerDue(t)&&!this.syncing.has(t.id)&&(this.syncing.add(t.id),flushTracker(this.bp,t.id).catch(()=>{}).finally(()=>this.syncing.delete(t.id)));let ls=t.localSetup;(ls?.status==="pending"||ls?.status==="running")&&!this.localRuns.has(ls.dir)&&(this.localRuns.add(ls.dir),this.localSetup(t.id).catch(()=>{}).finally(()=>this.localRuns.delete(ls.dir)));let chatKey=`__chat:${t.id}`;if(t.chat?.stopping?(this.stops.get(chatKey)?.abort(),this.running.has(chatKey)||this.chatStopped(t.id)):t.chat?.open&&t.chat.thinking&&!this.running.has(chatKey)&&(pause2?this.chatWaits(t,pause2):this.launch(chatKey,()=>this.chatReply(t.id))),!isMine(t,meta)||wakeIfReady(this.bp,t)||resumeAccept(this.bp,t))continue;if(mergeWait(t)&&this.lookAtMerge(t.id),openChange(t)?.status==="writing"&&!this.running.has(`__change:${t.id}`)&&this.launch(`__change:${t.id}`,()=>this.writeChange(t.id)),t.stopRequested){this.stops.get(t.id)?.abort(),this.stops.get(`__before:${t.id}`)?.abort(),this.running.has(t.id)||this.stopped(t.id);continue}if(this.running.has(t.id))continue;let q4=t.questions?.find(x=>x.thinking);if(q4){pause2?this.hold(t,limitText(pause2)):this.mayStart(limit)?this.launch(t.id,()=>this.reply(t.id,q4.id)):this.queue(t,limit);continue}if(t.squad&&Date.parse(t.squad.nextAt)<=Date.now()){let first2=meta.scan?.empty?firstOpenTicket(this.bp,t):void 0;if(first2)this.hold(t,waitsForFirstWords(first2.id));else if(pause2)this.hold(t,limitText(pause2));else if(this.mayStart(limit)){let phase=catchUp2(this.bp,t,this.phaseFor(t));this.launch(t.id,()=>this.step(t.id,phase))}else this.queue(t,limit)}else t.squad||this.pickUpChange(t)}this.buildQueue(limit,meta,pause2)}lookAtMerge(id2){let last=this.mergeLooks.get(id2);last!==void 0&&(last<0||Date.now()-last<2*6e4)||(this.mergeLooks.set(id2,-1),recheckMerge(this.bp,id2).then(r=>r==="clash"?this.pullRequestClash(id2):void 0).catch(()=>{}).finally(()=>this.mergeLooks.set(id2,Date.now())))}async pullRequestClash(id2){await fetchBase(this.bp,pullRequestClashed(this.bp,id2)),afterPullRequest(this.bp,id2,!0)}buildQueue(limit,meta,pause2){if(this.queueing||holding)return;this.queueing=!0;let free=()=>{let others=0;for(let s of squads)s!==this&&(others+=s.ticketsRunning());return placesLeft(limit,listTickets(this.bp),meta,others,[...waiting].some(s=>s!==this))};fillBuildQueue(this.bp,{free,pause:pause2,payment:async()=>(await howPaid(this.bp)).payment}).then(turns=>turns.forEach(x=>x.started.length&&this.log("info",`Build continuously started ${x.started.join(", ")} from ${x.id}`))).catch(err2=>this.log("warn",`Build continuously could not start a story: ${err2.message}`)).finally(()=>this.queueing=!1)}queue(t,limit){let running3=[];for(let s of squads)running3.push(...s.ticketIds());this.hold(t,queuedText(running3,limit))}hold(t,text3){if(!(t.status===text3&&!t.quiet)){held2(t.status)||this.beforeQueue.set(t.id,t.status);try{this.update(t.id,x=>{x.status=text3,delete x.quiet,pauseClock(x)})}catch{}}}unqueue(id2){let said=this.beforeQueue.get(id2);this.beforeQueue.delete(id2);try{if(!held2(readTicket(this.bp,id2)?.status))return;this.update(id2,x=>x.status=said??"Carrying on")}catch{}}waitOutBlip(id2,phase,err2){let tries=readTicket(this.bp,id2)?.blips??0,minutes4=BLIP_WAITS_MIN[tries];if(minutes4===void 0)return this.update(id2,x=>delete x.blips),this.fail(id2,err2.message,phase,err2.detail);this.log("warn",`A network or service problem at ${phase}, so it runs again in ${minutes4} min: ${err2.detail??err2.message}`,id2);try{this.update(id2,x=>{if(x.takenOver||x.chat?.open&&!x.chat.beside)return;let next=phase;phase==="review"&&x.reviewNotes?.length?next="review-fix":phase==="review"&&x.reviewRound&&(x.reviewRound-=1),phase==="test"&&(x.tries=Math.max(0,(x.tries??0)-1),x.fails=Math.max(0,(x.fails??0)-1)),x.blips=tries+1,x.live=!1,x.squad={...x.squad??{},phase:next,nextAt:new Date(Date.now()+minutes4*6e4).toISOString()},x.status=`Waiting for the connection. Tries again in ${minutes4} ${minutes4===1?"minute":"minutes"}`,delete x.quiet,tries||addActivity(x,"squad","The connection or the AI service had a problem, so squad-kit tries again by itself. Not a failed try")})}catch{}}launch(key,job){if(holding||key.startsWith("__")&&activePause())return;key.startsWith("__")||this.unqueue(key),this.running.add(key),this.stops.set(key,new AbortController),this.dog.beat(key);let phase=readTicket(this.bp,key)?.squad?.phase,mark=sleepMark();key.startsWith("__")||this.marks.set(key,mark),job().then(()=>{try{!key.startsWith("__")&&phase&&carriesOn(readTicket(this.bp,key)??{},phase)&&updateTicket(this.bp,key,x=>delete x.interrupted),!key.startsWith("__")&&readTicket(this.bp,key)?.blips&&updateTicket(this.bp,key,x=>delete x.blips)}catch{}}).catch(err2=>{if(this.closing||this.holdForSaving(err2)||key.startsWith("__")||readTicket(this.bp,key)?.deleting)return;if(readTicket(this.bp,key)?.stopRequested)return this.stopped(key,phase);if(err2 instanceof WaitingForYou)return;if(err2 instanceof LimitReached)return this.waitForLimit(key,phase??"analyst-read",err2.pause);if(err2 instanceof CapReached)return this.capPause(key,phase??"analyst-read",err2);if(err2 instanceof PassingTrouble)return this.waitOutBlip(key,phase??"analyst-read",err2);if(err2 instanceof CutOff||sleptSince(mark))return this.carryOnAfterSleep(key,phase??"analyst-read");let{what,detail}=explainError(err2),own2=isDiskTrouble(err2)?fileTroubleWords(err2):ownWords2(err2);own2?this.fail(key,own2,phase??"analyst-read",detail):this.fail(key,TECHNICAL_STOP,phase??"analyst-read",[what,detail].filter(Boolean).join(`
1707
- `))}).finally(async()=>{key.startsWith("__")||await this.clearAfterStep(key).catch(()=>{}),this.running.delete(key),this.stops.delete(key),this.marks.delete(key),this.dog.forget(key),key.startsWith("__")||this.setQuiet(key,null)})}carryOnAfterSleep(id2,phase){try{this.update(id2,x=>{if(x.takenOver||x.chat?.open&&!x.chat.beside)return;let next=markInterrupted(x,phase,"sleep");x.live=!0,delete x.quiet,x.squad={...x.squad??{},phase:next,nextAt:now()},addActivity(x,"squad","The computer slept and cut off the step, so it carries on from where it was. Not a failed try")})}catch{}}busyCopies(except,runs2=!0){let tickets=listTickets(this.bp),out18=tickets.filter(t=>t.takenOver).flatMap(t=>[t.takenOver.cwd,...ticketCopies(this.bp,t)]).filter(Boolean);for(let k of runs2?this.running:[]){if(k==="__setup"&&out18.push(...projectCopies(this.bp).map(r=>path116.join(r,"setup-copy"))),k.startsWith("__before:")){let t2=tickets.find(x=>x.id===k.slice(9));t2&&out18.push(...ticketCopies(this.bp,t2).filter(d=>d!==workFolder(t2)))}let t=!k.startsWith("__")&&k!==except?tickets.find(x=>x.id===k):void 0;t&&out18.push(...ticketCopies(this.bp,t))}return productHold(this.bp)&&out18.push(productCopiesDir(this.bp)),out18}async clearCopies(dirs,opts,forId){let found2=await stopLeftovers(dirs,opts);if(!found2.length)return found2;let tickets=listTickets(this.bp),ids=tickets.map(t=>t.id);this.log("info",`Stopped ${found2.length} program${found2.length===1?"":"s"} left running in squad-kit's copies: ${found2.map(p=>`${p.pid} ${p.command.slice(0,80)} in ${p.cwd}`).join("; ")}`,forId);let say=(id2,list2)=>{let t=tickets.find(x=>x.id===id2),lines3=stoppedLines(list2,cwd=>copyWords(this.bp,cwd,t,ids));try{lines3.length&&updateTicket(this.bp,id2,x=>lines3.forEach(l=>addActivity(x,"squad",l)))}catch{}};if(forId)say(forId,found2);else for(let t of tickets){let mine2=found2.filter(p=>folderOf(p.cwd,ticketCopies(this.bp,t)));mine2.length&&say(t.id,mine2)}return found2}async clearAfterStep(id2){if(this.closing)return;let t=readTicket(this.bp,id2);!t?.worktree||t.takenOver||!fs124.existsSync(t.worktree)||(stopProduct(this.bp,id2),await runsEnded([workFolder(t)],this.kept.has(id2)?0:6e3),await this.clearCopies([workFolder(t)],{},id2))}async clearBeforeApp(id2){let t=readTicket(this.bp,id2);t?.worktree&&(await this.clearCopies([workFolder(t)],{},id2),await this.clearCopies(projectCopies(this.bp),{busy:[workFolder(t),...this.busyCopies(id2)],listening:!0},id2))}keepApp(id2,cwd,app,tree){if(this.closing)return!1;let old=this.kept.get(id2);old?.leave(),old&&old.app!==app&&old.app.stop();let leave=appIn(cwd,async()=>this.kept.get(id2)?.app!==app?!1:(this.log("info",`squad-kit stops the app it kept at ${app.url}, so the checks in its copy can start their own`,id2),await this.dropKept(id2),!0));return this.kept.set(id2,{app,cwd,...tree?{tree}:{},at:Date.now(),leave}),this.log("info",`squad-kit keeps the app at ${app.url} running for the next step that uses it`,id2),!0}async keptFor(id2,cwd){let k=this.kept.get(id2);if(!k)return null;let real2=dir=>{try{return fs124.realpathSync.native(dir)}catch{return path116.resolve(dir)}},why="";return k.tree?real2(k.cwd)!==real2(cwd)?why="it runs from another copy":k.app.ended?.()?why="it stopped":await answersAt(k.app.url)?await treeOf(cwd)!==k.tree&&(why="the copy's code changed since it started"):why="it does not answer any more":why="it was not kept for this step",this.kept.get(id2)!==k?null:why?(this.log("info",`squad-kit starts the app again: the one it kept ${why}`,id2),await this.dropKept(id2),null):(this.kept.delete(id2),k.leave(),{app:k.app,tree:k.tree})}async dropKept(id2){let k=this.kept.get(id2);k&&(this.kept.delete(id2),k.leave(),k.app.stop(),this.log("info",`squad-kit stopped the app it kept at ${k.app.url}`,id2),await untilEnded(k.app))}async afterReview(id2){let k=this.kept.get(id2);if(!k)return;let t=readTicket(this.bp,id2),tree=!!(t&&t.gates?.review==="pass"&&t.squad?.phase==="proof"&&!t.stopRequested&&!this.stops.get(id2)?.signal.aborted)&&!k.app.ended?.()?await treeOf(k.cwd):void 0;if(this.kept.get(id2)===k){if(!tree)return this.dropKept(id2);k.tree=tree,k.at=Date.now()}}sweepKept(){for(let[id2,k]of this.kept){let t=null;try{t=readTicket(this.bp,id2)}catch{}let runs2=this.running.has(id2);(!(t&&!t.deleting&&!t.stopRequested&&(runs2||t.squad?.phase==="proof"))||k.app.ended?.()||!runs2&&Date.now()-k.at>KEPT_APP_MS)&&this.dropKept(id2)}}watch(){if(Date.now()-this.lastWatch<15e3)return;this.lastWatch=Date.now();let slept=this.dog.resumed(),keys=[...this.running].filter(k=>!k.startsWith("__"));if(!keys.length)return;if(slept){let m=Math.max(1,Math.round(slept/6e4));for(let id2 of keys)try{this.update(id2,x=>{addActivity(x,"squad",`Resumed after sleep: the computer slept for about ${m} minute${m===1?"":"s"}`),delete x.quiet})}catch{}return}let rows2=processTable(),usual=readUsual(this.bp);for(let id2 of keys){let t=readTicket(this.bp,id2);if(!t)continue;let snoozed=t.quietSnoozedUntil&&Date.parse(t.quietSnoozedUntil)>Date.now();this.setQuiet(id2,snoozed?null:this.dog.check(id2,Date.now(),rows2)??slowStep(t,usual)),watchPort(this.bp,id2)}}pickUp(){this.pickingUp||Date.now()-this.lastPickUp<PICK_UP_MS||(this.pickingUp=!0,this.lastPickUp=Date.now(),fetchPushed(this.bp).catch(()=>{}).finally(()=>this.pickingUp=!1))}setQuiet(id2,q4){try{let t=readTicket(this.bp,id2);if(!t||!q4&&!t.quiet||q4&&t.quiet&&q4.kind===t.quiet.kind&&q4.detail===t.quiet.detail)return;this.update(id2,x=>{q4&&x.quiet?.kind!==q4.kind&&addActivity(x,"squad",q4.kind==="stuck"?`Looks stuck: ${q4.detail}`:`Taking long: ${q4.detail}`),q4?x.quiet={...q4,...x.quiet?.port&&q4.kind===x.quiet.kind?{port:x.quiet.port}:{}}:delete x.quiet})}catch{}}letGo(t){let keys=[t.id,`__chat:${t.id}`,`__change:${t.id}`,`__before:${t.id}`];for(let k of keys)this.stops.get(k)?.abort();if(!(t.deleting?.stopped||keys.some(k=>this.running.has(k))||this.syncing.has(t.id)))try{updateTicket(this.bp,t.id,x=>{x.deleting&&(x.deleting.stopped=now())})}catch{}}stopped(id2,phase){let held4=readTicket(this.bp,id2);if(held4?.mergedAt){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0});return}if(held4?.takenOver||held4?.chat?.open&&!held4.chat.beside){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0,t.live=!1,t.squad=null,addActivity(t,"squad","Stopped the running step for you")});return}if(held4?.retryOnStop){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0,delete t.quiet,delete t.clock,t.live=!0,t.status="Trying the step again",addActivity(t,"squad","Stopped the step and started it again, with the work so far"),scheduleSquad(t,phase??t.squad?.phase??t.retryPhase??"analyst-read",.2,1)});return}let at3=phase??held4?.squad?.phase??held4?.retryPhase??"analyst-read";phase&&this.update(id2,t=>at3=markInterrupted(t,phase,"stop")),this.fail(id2,"You stopped the squad. Nothing was lost; the work so far is kept.",at3),this.update(id2,t=>{t.stopRequested=void 0,t.stuck&&(t.stuck.options[0]={name:"Carry on",note:"Run the stopped step again."})})}commandWatch(id2){let signal2=this.stops.get(id2)?.signal;return{onSpawn:pid=>this.dog.track(id2,pid),onOutput:()=>this.dog.beat(id2),onHealed:detail=>this.healed(id2,detail),...signal2?{signal:signal2}:{}}}healed(id2,detail){this.log("info",detail,id2);try{this.update(id2,x=>addActivity(x,"squad",HEALED_LINE))}catch{}}guardFor(cwd,id2){if(!guardSupport().on)return;let allow=this.meta().guard;return ticketGuard(this.bp,{cwd,id:id2,...allow?{allow}:{}})}guardedWatch(id2,cwd){let guard=this.guardFor(cwd,id2);return{...this.commandWatch(id2),...guard?{guard}:{}}}async makingCopy(id2,make){let said=readTicket(this.bp,id2)?.status,signal2=this.stops.get(id2)?.signal,beat2=setInterval(()=>this.dog.beat(id2),3e4);try{return await make({...signal2?{signal:signal2}:{},making:()=>this.update(id2,x=>x.status=DOING.copy)})}finally{clearInterval(beat2),said!==void 0&&readTicket(this.bp,id2)?.status===DOING.copy&&this.update(id2,x=>x.status=said)}}goOn(id2){if(this.stops.get(id2)?.signal.aborted)throw new Error(STOPPED10)}meta(){return readMeta(this.bp)}ctx(t){let meta=this.meta(),copies=t?.storyDir?copiesOf(meta,t):[];return{rules:this.ruleCopies().files,...t?{repo:this.appOf(t)?.repo??repoFor(meta,t)}:{},...copies.length?{copies:copies.map(c=>({folder:c.repo.path,name:c.label,commands:c.repo.commands,...c.baseCommit?{baseCommit:c.baseCommit}:{}}))}:{}}}extraDirs(t){let rules=this.ruleCopies();return[...attachmentDirs(t),...rules.files.length?[rules.dir]:[]]}ruleCopies(){let root=projectRoot(this.bp),dir=path116.join(this.bp.localDir,"rules-read"),files=rulesFiles(root).map(from=>{let to=path116.join(dir,path116.relative(root,from));return fs124.mkdirSync(path116.dirname(to),{recursive:!0}),fs124.copyFileSync(from,to),to});return{dir,files}}update(id2,change){return updateTicket(this.bp,id2,t=>{change(t),t.takenOver?(t.waiting=t.takenOver.free?{kind:"yours",summary:OUTSIDE_WAITS}:{kind:"yours",summary:"You took it over. Give it back when you are done"},t.squad=null,t.live=!1):t.chat?.open&&!t.chat.beside&&(t.waiting={kind:"chat",summary:t.waiting?.kind==="chat"?t.waiting.summary:"Talking it through"},t.squad=null,t.live=!1)})}watcher(id2,role,cwd,onLine){let last=0,logged=0;return e=>{let line3=describeToolEvent(e,cwd);if(!line3||Date.now()-last<700)return;last=Date.now();let plain=plainToolLine(e,cwd)??DOING.other;this.update(id2,t=>{t.status=plain,t.live=!0,stepDoing(t,plain),logged<40&&/^(Editing|Writing) /.test(line3)&&e.name!=="Bash"&&isInside(cwd,e.input.file_path)&&(addActivity(t,role,line3),logged+=1),onLine?.(plain,t)})}}toolLabel(role){return aiTool(readAgent(this.bp,role).tool)?.label??"its AI tool"}async runAgent(opts){let job=opts.job??opts.ticket,jobSignal=job?this.stops.get(job)?.signal:void 0,runStop2=new AbortController,onJobStop=()=>runStop2.abort();jobSignal?.aborted&&runStop2.abort(),jobSignal?.addEventListener("abort",onJobStop,{once:!0});let signal2=runStop2.signal,mine2=!opts.win&&opts.ticket?this.windowFor(opts,signal2):void 0,win=opts.win??mine2;win?.whenTooMany(()=>runStop2.abort()),mine2&&openWindow(mine2);try{return await this.runWith({...opts,...win?{win}:{}},signal2)}finally{jobSignal?.removeEventListener("abort",onJobStop),mine2&&await closeWindow(mine2)}}windowFor(opts,signal2){let id2=opts.ticket,t=readTicket(this.bp,id2),appCopy=t?this.appOf(t):void 0,rc=appCopy?.repo.commands??(t&&!runsAsProduct(this.meta())?repoFor(this.meta(),t).commands:{}),inCopy=!!(t?.worktree&&opts.cwd===workFolder(t)),here=inCopy&&!!(rc.start?.trim()&&rc.url?.trim()),builds=inCopy&&opts.permissions.includes("edit"),app=here&&builds,look=here&&opts.role==="reviewer"&&opts.step==="review",noted=opts.role==="builder"&&!opts.job,who4=AGENTS.find(a=>a.role===opts.role)?.name??opts.role;return new AgentWindow({...this.windowBase(id2,opts.role,opts.cwd,opts.job??id2),step:opts.step??"chat",who:who4,tools:toolsFor({app,look,checks:builds,parts:builds&&!!t?.storyDir}),...app||look?{app:{cwd:appCopy.dir,start:rc.start.trim(),url:rc.url.trim(),...runsAsProduct(this.meta())||!this.guardFor(appCopy.dir,id2)?{}:{guard:this.guardFor(appCopy.dir,id2)},...this.productApp(id2)}}:{},...builds?{checks:t?.storyDir?this.windowChecksByPart(id2,t,noted):this.windowChecks(id2,opts.cwd,void 0,noted)}:{},...builds&&t?.storyDir?{addPart:part=>this.addPart(id2,part,this.lanesOf.get(id2))}:{},...look&&!runsAsProduct(this.meta())?{keep:a=>this.keepApp(id2,appCopy.dir,a)}:{},prefix:`${id2.toLowerCase()}-${opts.role}`,signal:signal2})}windowChecks(id2,cwd,repo,noted=!1){let guard=this.guardFor(cwd,id2);return{cwd,commands:()=>this.saveFoundCommands(id2,cwd,repo),...guard?{guard}:{},onStart:name=>this.update(id2,x=>{x.status=CHECK_DOING[name],stepDoing(x,x.status)}),...noted?{onRun:run6=>this.noteRun(id2,cwd,run6)}:{}}}async noteRun(id2,cwd,run6){let notes=this.builderRuns.get(id2);for(let name of run6.names)notes?.get(cwd)?.delete(name);let tree=notes?await treeOf(cwd):void 0;if(!notes||!tree)return;let at3=Date.now();return async ran2=>{let passed=ran2.filter(r=>r.ok&&!r.ended&&!r.only);if(!passed.length||await treeOf(cwd)!==tree||this.builderRuns.get(id2)!==notes)return;let here=notes.get(cwd)??new Map;for(let r of passed)here.set(r.name,{tree,run:{name:r.name,command:r.command,ok:r.ok,output:r.output,seconds:r.seconds},at:at3});notes.set(cwd,here)}}appOf(t){let meta=this.meta();return runsAsProduct(meta)?productPlan(this.bp,meta,t).front:appCopyOf(meta,t)}productApp(id2){return runsAsProduct(this.meta())?{run:()=>this.startProductFor(id2),after:()=>stopProduct(this.bp,id2),reach:()=>productPlan(this.bp,this.meta(),readTicket(this.bp,id2)).places.map(p=>p.url)}:{}}startProductFor(id2){let signal2=this.stops.get(id2)?.signal;return startProduct(this.bp,readTicket(this.bp,id2),id2,"story",{...signal2?{signal:signal2}:{},onWait:hold=>this.update(id2,x=>x.status=waitWords2(hold)),log:text3=>this.log("info",text3,id2)})}apiParts(t){let meta=this.meta();if(runsAsProduct(meta)){let{places}=productPlan(this.bp,meta,t),beside=partsOf(meta).filter(p=>p.url&&!p.shared&&!p.off&&!p.noRun&&places.some(x=>x.repo.path===p.repo&&x.part.id!==p.id));return Object.fromEntries([...places.map(p=>[p.part.id,{name:p.part.name,url:p.url}]),...beside.map(p=>[p.id,{name:p.name,url:p.url}])])}let c=(this.appOf(t)?.repo??repoFor(meta,t)).commands;return c.start?.trim()&&c.url?.trim()?{app:{name:"the app",url:c.url.trim()}}:{}}windowChecksByPart(id2,t,noted=!1){let parts2=()=>{let meta=this.meta(),now2=readTicket(this.bp,id2)??t,copies=copiesOf(meta,now2),out18={};for(let c of copies){let named=partsOf(meta).filter(p=>p.repo===c.repo.path&&!p.shared);for(let key of named.length?named.map(p=>p.id):[c.repo.path])out18[key]=this.windowChecks(id2,c.dir,c.repo.path,noted)}return out18};return{...this.windowChecks(id2,t.worktree,repoFor(this.meta(),t).path,noted),parts:parts2}}windowBase(id2,role,cwd,job=id2){return{ticket:id2,proofDir:this.bp.proofDir,beat:()=>this.dog.beat(job),say:text3=>{try{this.update(id2,x=>addActivity(x,role,text3))}catch{}},log:text3=>this.log("warn",text3,id2),beforeStart:async()=>{await this.clearCopies(projectCopies(this.bp),{busy:[cwd,...this.busyCopies(id2)],listening:!0},id2);let t=role==="builder"?null:readTicket(this.bp,id2),c=t?copiesOf(this.meta(),t).find(x=>path116.resolve(x.dir)===path116.resolve(cwd)):void 0,failed2=c?await this.setUpAgain(id2,c):null;failed2&&this.log("warn",`Setting up the copy again did not work, so the app may not work (${failed2.command}):
1707
+ `))}).finally(async()=>{key.startsWith("__")||await this.clearAfterStep(key).catch(()=>{}),this.running.delete(key),this.stops.delete(key),this.marks.delete(key),this.dog.forget(key),key.startsWith("__")||this.setQuiet(key,null)})}carryOnAfterSleep(id2,phase){try{this.update(id2,x=>{if(x.takenOver||x.chat?.open&&!x.chat.beside)return;let next=markInterrupted(x,phase,"sleep");x.live=!0,delete x.quiet,x.squad={...x.squad??{},phase:next,nextAt:now()},addActivity(x,"squad","The computer slept and cut off the step, so it carries on from where it was. Not a failed try")})}catch{}}busyCopies(except,runs2=!0){let tickets=listTickets(this.bp),out18=tickets.filter(t=>t.takenOver).flatMap(t=>[t.takenOver.cwd,...ticketCopies(this.bp,t)]).filter(Boolean);for(let k of runs2?this.running:[]){if(k==="__setup"&&out18.push(...projectCopies(this.bp).map(r=>path116.join(r,"setup-copy"))),k.startsWith("__before:")){let t2=tickets.find(x=>x.id===k.slice(9));t2&&out18.push(...ticketCopies(this.bp,t2).filter(d=>d!==workFolder(t2)))}let t=!k.startsWith("__")&&k!==except?tickets.find(x=>x.id===k):void 0;t&&out18.push(...ticketCopies(this.bp,t))}return productHold(this.bp)&&out18.push(productCopiesDir(this.bp)),out18}async clearCopies(dirs,opts,forId){let found2=await stopLeftovers(dirs,opts);if(!found2.length)return found2;let tickets=listTickets(this.bp),ids=tickets.map(t=>t.id);this.log("info",`Stopped ${found2.length} program${found2.length===1?"":"s"} left running in squad-kit's copies: ${found2.map(p=>`${p.pid} ${p.command.slice(0,80)} in ${p.cwd}`).join("; ")}`,forId);let say=(id2,list2)=>{let t=tickets.find(x=>x.id===id2),lines3=stoppedLines(list2,cwd=>copyWords(this.bp,cwd,t,ids));try{lines3.length&&updateTicket(this.bp,id2,x=>lines3.forEach(l=>addActivity(x,"squad",l)))}catch{}};if(forId)say(forId,found2);else for(let t of tickets){let mine2=found2.filter(p=>folderOf(p.cwd,ticketCopies(this.bp,t)));mine2.length&&say(t.id,mine2)}return found2}async clearAfterStep(id2){if(this.closing)return;let t=readTicket(this.bp,id2);!t?.worktree||t.takenOver||!fs124.existsSync(t.worktree)||(stopProduct(this.bp,id2),await runsEnded([workFolder(t)],this.kept.has(id2)?0:6e3),await this.clearCopies([workFolder(t)],{},id2))}async clearBeforeApp(id2){let t=readTicket(this.bp,id2);t?.worktree&&(await this.clearCopies([workFolder(t)],{},id2),await this.clearCopies(projectCopies(this.bp),{busy:[workFolder(t),...this.busyCopies(id2)],listening:!0},id2))}keepApp(id2,cwd,app,tree){if(this.closing)return!1;let old=this.kept.get(id2);old?.leave(),old&&old.app!==app&&old.app.stop();let leave=appIn(cwd,async()=>this.kept.get(id2)?.app!==app?!1:(this.log("info",`squad-kit stops the app it kept at ${app.url}, so the checks in its copy can start their own`,id2),await this.dropKept(id2),!0));return this.kept.set(id2,{app,cwd,...tree?{tree}:{},at:Date.now(),leave}),this.log("info",`squad-kit keeps the app at ${app.url} running for the next step that uses it`,id2),!0}async keptFor(id2,cwd){let k=this.kept.get(id2);if(!k)return null;let real2=dir=>{try{return fs124.realpathSync.native(dir)}catch{return path116.resolve(dir)}},why="";return k.tree?real2(k.cwd)!==real2(cwd)?why="it runs from another copy":k.app.ended?.()?why="it stopped":await answersAt(k.app.url)?await treeOf(cwd)!==k.tree&&(why="the copy's code changed since it started"):why="it does not answer any more":why="it was not kept for this step",this.kept.get(id2)!==k?null:why?(this.log("info",`squad-kit starts the app again: the one it kept ${why}`,id2),await this.dropKept(id2),null):(this.kept.delete(id2),k.leave(),{app:k.app,tree:k.tree})}async dropKept(id2){let k=this.kept.get(id2);k&&(this.kept.delete(id2),k.leave(),k.app.stop(),this.log("info",`squad-kit stopped the app it kept at ${k.app.url}`,id2),await untilEnded(k.app))}async afterReview(id2){let k=this.kept.get(id2);if(!k)return;let t=readTicket(this.bp,id2),tree=!!(t&&t.gates?.review==="pass"&&t.squad?.phase==="proof"&&!t.stopRequested&&!this.stops.get(id2)?.signal.aborted)&&!k.app.ended?.()?await treeOf(k.cwd):void 0;if(this.kept.get(id2)===k){if(!tree)return this.dropKept(id2);k.tree=tree,k.at=Date.now()}}sweepKept(){for(let[id2,k]of this.kept){let t=null;try{t=readTicket(this.bp,id2)}catch{}let runs2=this.running.has(id2);(!(t&&!t.deleting&&!t.stopRequested&&(runs2||t.squad?.phase==="proof"))||k.app.ended?.()||!runs2&&Date.now()-k.at>KEPT_APP_MS)&&this.dropKept(id2)}}watch(){if(Date.now()-this.lastWatch<15e3)return;this.lastWatch=Date.now();let slept=this.dog.resumed(),keys=[...this.running].filter(k=>!k.startsWith("__"));if(!keys.length)return;if(slept){let m=Math.max(1,Math.round(slept/6e4));for(let id2 of keys)try{this.update(id2,x=>{addActivity(x,"squad",`Resumed after sleep: the computer slept for about ${m} minute${m===1?"":"s"}`),delete x.quiet})}catch{}return}let rows2=processTable(),usual=readUsual(this.bp);for(let id2 of keys){let t=readTicket(this.bp,id2);if(!t)continue;let snoozed=t.quietSnoozedUntil&&Date.parse(t.quietSnoozedUntil)>Date.now();this.setQuiet(id2,snoozed?null:this.dog.check(id2,Date.now(),rows2)??slowStep(t,usual)),watchPort(this.bp,id2)}}pickUp(){this.pickingUp||Date.now()-this.lastPickUp<PICK_UP_MS||(this.pickingUp=!0,this.lastPickUp=Date.now(),fetchPushed(this.bp).catch(()=>{}).finally(()=>this.pickingUp=!1))}setQuiet(id2,q4){try{let t=readTicket(this.bp,id2);if(!t||!q4&&!t.quiet||q4&&t.quiet&&q4.kind===t.quiet.kind&&q4.detail===t.quiet.detail)return;this.update(id2,x=>{q4&&x.quiet?.kind!==q4.kind&&addActivity(x,"squad",q4.kind==="stuck"?`Looks stuck: ${q4.detail}`:`Taking long: ${q4.detail}`),q4?x.quiet={...q4,...x.quiet?.port&&q4.kind===x.quiet.kind?{port:x.quiet.port}:{}}:delete x.quiet})}catch{}}letGo(t){let keys=[t.id,`__chat:${t.id}`,`__change:${t.id}`,`__before:${t.id}`];for(let k of keys)this.stops.get(k)?.abort();if(!(t.deleting?.stopped||keys.some(k=>this.running.has(k))||this.syncing.has(t.id)))try{updateTicket(this.bp,t.id,x=>{x.deleting&&(x.deleting.stopped=now())})}catch{}}stopped(id2,phase){let held4=readTicket(this.bp,id2);if(held4?.mergedAt){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0});return}if(held4?.takenOver||held4?.chat?.open&&!held4.chat.beside){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0,t.live=!1,t.squad=null,addActivity(t,"squad","Stopped the running step for you")});return}if(held4?.retryOnStop){this.update(id2,t=>{t.stopRequested=void 0,t.retryOnStop=void 0,delete t.quiet,delete t.clock,t.live=!0,t.status="Trying the step again",addActivity(t,"squad","Stopped the step and started it again, with the work so far"),scheduleSquad(t,phase??t.squad?.phase??t.retryPhase??"analyst-read",.2,1)});return}let at3=phase??held4?.squad?.phase??held4?.retryPhase??"analyst-read";phase&&this.update(id2,t=>at3=markInterrupted(t,phase,"stop")),this.fail(id2,"You stopped the squad. Nothing was lost; the work so far is kept.",at3),this.update(id2,t=>{t.stopRequested=void 0,t.stuck&&(t.stuck.options[0]={name:"Carry on",note:"Run the stopped step again."})})}commandWatch(id2){let signal2=this.stops.get(id2)?.signal;return{onSpawn:pid=>this.dog.track(id2,pid),onOutput:()=>this.dog.beat(id2),onHealed:detail=>this.healed(id2,detail),...signal2?{signal:signal2}:{}}}healed(id2,detail){this.log("info",detail,id2);try{this.update(id2,x=>addActivity(x,"squad",HEALED_LINE))}catch{}}guardFor(cwd,id2){if(!guardSupport().on)return;let allow=this.meta().guard;return ticketGuard(this.bp,{cwd,id:id2,...allow?{allow}:{}})}guardedWatch(id2,cwd){let guard=this.guardFor(cwd,id2);return{...this.commandWatch(id2),...guard?{guard}:{}}}async makingCopy(id2,make){let said=readTicket(this.bp,id2)?.status,signal2=this.stops.get(id2)?.signal,beat2=setInterval(()=>this.dog.beat(id2),3e4);try{return await make({...signal2?{signal:signal2}:{},making:()=>this.update(id2,x=>x.status=DOING.copy)})}finally{clearInterval(beat2),said!==void 0&&readTicket(this.bp,id2)?.status===DOING.copy&&this.update(id2,x=>x.status=said)}}goOn(id2){if(this.stops.get(id2)?.signal.aborted)throw new Error(STOPPED10)}meta(){return readMeta(this.bp)}ctx(t){let meta=this.meta(),copies=t?.storyDir?copiesOf(meta,t):[];return{rules:this.ruleCopies().files,...t?{repo:this.appOf(t)?.repo??repoFor(meta,t)}:{},...copies.length?{copies:copies.map(c=>({folder:c.repo.path,name:c.label,commands:c.repo.commands,...c.baseCommit?{baseCommit:c.baseCommit}:{}}))}:{}}}extraDirs(t){let rules=this.ruleCopies();return[...attachmentDirs(t),...rules.files.length?[rules.dir]:[]]}ruleCopies(){let root=projectRoot(this.bp),dir=path116.join(this.bp.localDir,"rules-read"),files=rulesFiles(root).map(from=>{let to=path116.join(dir,path116.relative(root,from));return fs124.mkdirSync(path116.dirname(to),{recursive:!0}),fs124.copyFileSync(from,to),to});return{dir,files}}update(id2,change){return updateTicket(this.bp,id2,t=>{change(t),t.takenOver?(t.waiting=t.takenOver.free?{kind:"yours",summary:OUTSIDE_WAITS}:{kind:"yours",summary:"You took it over. Give it back when you are done"},t.squad=null,t.live=!1):t.chat?.open&&!t.chat.beside&&(t.waiting={kind:"chat",summary:t.waiting?.kind==="chat"?t.waiting.summary:"Talking it through"},t.squad=null,t.live=!1)})}watcher(id2,role,cwd,onLine){let last=0,logged=0;return e=>{let line3=describeToolEvent(e,cwd);if(!line3||Date.now()-last<700)return;last=Date.now();let plain=plainToolLine(e,cwd)??DOING.other;this.update(id2,t=>{t.status=plain,t.live=!0,stepDoing(t,plain),logged<40&&/^(Editing|Writing) /.test(line3)&&e.name!=="Bash"&&isInside(cwd,e.input.file_path)&&(addActivity(t,role,line3),logged+=1),onLine?.(plain,t)})}}toolLabel(role){return aiTool(readAgent(this.bp,role).tool)?.label??"its AI tool"}async runAgent(opts){let job=opts.job??opts.ticket,jobSignal=job?this.stops.get(job)?.signal:void 0,runStop2=new AbortController,onJobStop=()=>runStop2.abort();jobSignal?.aborted&&runStop2.abort(),jobSignal?.addEventListener("abort",onJobStop,{once:!0});let signal2=runStop2.signal,mine2=!opts.win&&opts.ticket?this.windowFor(opts,signal2):void 0,win=opts.win??mine2;win?.whenTooMany(()=>runStop2.abort()),mine2&&openWindow(mine2);try{return await this.runWith({...opts,...win?{win}:{}},signal2)}finally{jobSignal?.removeEventListener("abort",onJobStop),mine2&&await closeWindow(mine2)}}windowFor(opts,signal2){let id2=opts.ticket,t=readTicket(this.bp,id2),appCopy=t?this.appOf(t):void 0,rc=appCopy?.repo.commands??(t&&!runsAsProduct(this.meta())?repoFor(this.meta(),t).commands:{}),inCopy=!!(t?.worktree&&opts.cwd===workFolder(t)),here=inCopy&&!!(rc.start?.trim()&&rc.url?.trim()),builds=inCopy&&opts.permissions.includes("edit"),app=here&&builds,look=here&&opts.role==="reviewer"&&opts.step==="review",noted=opts.role==="builder"&&!opts.job,who4=AGENTS.find(a=>a.role===opts.role)?.name??opts.role;return new AgentWindow({...this.windowBase(id2,opts.role,opts.cwd,opts.job??id2),step:opts.step??"chat",who:who4,tools:toolsFor({app,look,checks:builds,parts:builds&&!!t?.storyDir}),...app||look?{app:{cwd:appCopy.dir,start:rc.start.trim(),url:rc.url.trim(),...runsAsProduct(this.meta())||!this.guardFor(appCopy.dir,id2)?{}:{guard:this.guardFor(appCopy.dir,id2)},...this.productApp(id2)}}:{},...builds?{checks:t?.storyDir?this.windowChecksByPart(id2,t,noted):this.windowChecks(id2,opts.cwd,void 0,noted)}:{},...builds&&t?.storyDir?{addPart:part=>this.addPart(id2,part,this.lanesOf.get(id2))}:{},...look&&!runsAsProduct(this.meta())?{keep:a=>this.keepApp(id2,appCopy.dir,a)}:{},prefix:`${id2.toLowerCase()}-${opts.role}`,signal:signal2})}windowChecks(id2,cwd,repo,noted=!1){let guard=this.guardFor(cwd,id2);return{cwd,commands:()=>this.saveFoundCommands(id2,cwd,repo),...guard?{guard}:{},onStart:name=>this.update(id2,x=>{x.status=CHECK_DOING[name],stepDoing(x,x.status)}),...noted?{onRun:run6=>this.noteRun(id2,cwd,run6)}:{}}}async noteRun(id2,cwd,run6){let notes=this.builderRuns.get(id2);for(let name of run6.names)notes?.get(cwd)?.delete(name);let tree=notes?await treeOf(cwd):void 0;if(!notes||!tree)return;let at3=Date.now();return async ran2=>{let passed=ran2.filter(r=>r.ok&&!r.ended&&!r.only);if(!passed.length||await treeOf(cwd)!==tree||this.builderRuns.get(id2)!==notes)return;let here=notes.get(cwd)??new Map;for(let r of passed)here.set(r.name,{tree,run:{name:r.name,command:r.command,ok:r.ok,output:r.output,seconds:r.seconds},at:at3});notes.set(cwd,here)}}appOf(t){let meta=this.meta();return runsAsProduct(meta)?productPlan(this.bp,meta,t).front:appCopyOf(meta,t)}productApp(id2){return runsAsProduct(this.meta())?{run:()=>this.startProductFor(id2),after:()=>stopProduct(this.bp,id2),reach:()=>productPlan(this.bp,this.meta(),readTicket(this.bp,id2)).places.map(p=>p.url)}:{}}startProductFor(id2){let signal2=this.stops.get(id2)?.signal;return startProduct(this.bp,readTicket(this.bp,id2),id2,"story",{...signal2?{signal:signal2}:{},onWait:hold=>this.update(id2,x=>x.status=waitWords2(hold)),log:text3=>this.log("info",text3,id2)})}apiParts(t){let meta=this.meta();if(runsAsProduct(meta)){let{places}=productPlan(this.bp,meta,t),beside=partsOf(meta).filter(p=>p.url&&!p.shared&&!p.off&&!p.noRun&&places.some(x=>x.repo.path===p.repo&&x.part.id!==p.id));return Object.fromEntries([...places.map(p=>[p.part.id,{name:p.part.name,url:p.url}]),...beside.map(p=>[p.id,{name:p.name,url:p.url}])])}let c=(this.appOf(t)?.repo??repoFor(meta,t)).commands;return c.start?.trim()&&c.url?.trim()?{app:{name:"the app",url:c.url.trim()}}:{}}windowChecksByPart(id2,t,noted=!1){let parts2=()=>{let meta=this.meta(),now2=readTicket(this.bp,id2)??t,copies=copiesOf(meta,now2),out18={};for(let c of copies){let named=partsOf(meta).filter(p=>p.repo===c.repo.path&&!p.shared);for(let key of named.length?named.map(p=>p.id):[c.repo.path])out18[key]=this.windowChecks(id2,c.dir,c.repo.path,noted)}return out18};return{...this.windowChecks(id2,t.worktree,repoFor(this.meta(),t).path,noted),parts:parts2}}windowBase(id2,role,cwd,job=id2){return{ticket:id2,proofDir:this.bp.proofDir,beat:()=>this.dog.beat(job),say:text3=>{try{this.update(id2,x=>addActivity(x,role,text3))}catch{}},log:text3=>this.log("warn",text3,id2),beforeStart:async()=>{await this.clearCopies(projectCopies(this.bp),{busy:[cwd,...this.busyCopies(id2)],listening:!0},id2);let t=role==="builder"?null:readTicket(this.bp,id2),c=t?copiesOf(this.meta(),t).find(x=>path116.resolve(x.dir)===path116.resolve(cwd)):void 0,failed2=c?(await this.setUpAgain(id2,c)).failed:void 0;failed2&&this.log("warn",`Setting up the copy again did not work, so the app may not work (${failed2.command}):
1708
1708
  ${tail(failed2.output,30)}`,id2)},clearCopy:async(dir,only)=>!!folderOf(dir,projectCopies(this.bp))&&(await this.clearCopies([dir],only?{only:[only]}:{},id2)).length>0,inTheWay:async text3=>(await this.inTheWay(id2,{text:text3}))?.answer??null}}async runWith(opts,signal2){let job=opts.job??opts.ticket,agent=readAgent(this.bp,opts.role),tool5=aiTool(agent.tool);if(!tool5?.canRun){let can=AI_TOOLS.filter(t=>t.canRun).map(t=>t.label).join(" or ");throw new StepError(`The ${opts.role} agent is set to ${tool5?.label??agent.tool}, which squad-kit cannot run yet. Switch it to ${can} on the Harness page, under Agents.`)}let extra=agentInstructions(agent,opts.step),win=opts.win,boxed=!!(opts.box&&sandboxOf(tool5).available),network=boxed?projectNetwork(this.bp.root):void 0,instructions=[opts.instructions,PROCESS_RULE,extra,rulesFor(this.bp,opts.role),win?windowRule(win.tools):"",network?networkRule(network):""].filter(Boolean).join(`
1709
1709
 
1710
1710
  `),access=win?await windowAccess(win):void 0,key=opts.ticket,ticket=key?readTicket(this.bp,key):null,levelStep=opts.levelStep??opts.step??"chat",on={quality:this.meta().quality,express:ticket?.track==="express",fails:ticket?.fails},floor=opts.role==="reviewer"?{builder:builderLevel(on,readAgent(this.bp,"builder").levels,personLevels())}:{},lv=resolveLevel({role:opts.role,step:levelStep,...on,...floor},agent.levels,personLevels()),effort=effortOn(tool5,lv.effort);key&&levelStep!=="chat"&&this.update(key,x=>addActivity(x,opts.role,`Runs on ${describeLevel(lv,tool5)}`)),opts=this.withNewNotes(opts);let timed=key&&job===key?timedStep(opts.step??levelStep):null;timed&&this.update(key,x=>startStep(x,{who:opts.role,step:timed,limit:Math.round((opts.timeoutMs??RUN_MS)/6e4)}));let ended=res=>(timed&&this.update(key,x=>endStep(this.bp,x)),res),watch=job?{onActivity:()=>this.dog.beat(job),onSpawn:pid=>this.dog.track(job,pid)}:{},raise=opts.capMore??(ticket?.capRaise?.role===opts.role&&ticket.capRaise.step===levelStep?ticket.capRaise.times:0),quality=this.meta().quality??DEFAULT_QUALITY,caps=stepCaps({role:opts.role,step:levelStep,quality},raise),runCaps={...tool5.caps?.turns?{turns:caps.turns}:{},...tool5.caps?.spend?{spendUsd:caps.spendUsd}:{}},cache4=boxed&&key?storiesCacheDir(this.bp):void 0;cache4&&ownFolder(cache4);let sandbox=boxed?await this.sandboxFor(opts.cwd,opts.addDirs??[],cache4,network):void 0,secrets=await secretFiles(this.bp,opts.cwd),refused=new Set,onBlocked=b=>{!key||refused.has(`${b.kind}:${b.target}`)||refused.size>=10||(refused.add(`${b.kind}:${b.target}`),b.kind==="host"&&noteBlockedSite(this.bp,{host:b.target,story:key,level:network??projectNetwork(this.bp.root)}),this.update(key,x=>addActivity(x,opts.role,blockedLine(b))))},run6=({win:_own,reply:_reply,check:_check,checkHelp:_help,capMore:_more,...o})=>tool5.run({...o,...watch,secretFiles:secrets,privatePlaces:agentPrivatePlaces(),protectedFiles:this.protectedFiles(opts.cwd),...Object.keys(runCaps).length?{caps:runCaps}:{},...sandbox?{sandbox,onBlocked}:{},env:{...noPushEnv(),...sandbox&&cache4?cacheHomeEnv(cache4,{wall:"agent",cwd:opts.cwd}):{},...key?copyDbEnv(opts.cwd):{}},...instructions?{instructions}:{},level:lv.level,...effort?{effort}:{},...lv.model?{model:lv.model}:{},signal:signal2,...access?{window:access}:{},logDir:this.bp.chatsDir,direct:this.meta().runner==="direct",onFallback:why=>{this.log("warn",`ACP did not start, so the ${opts.role} runs ${tool5.label} directly: ${why}`,key),key&&this.update(key,x=>addActivity(x,"squad","squad-kit runs this step another way, as the usual way did not start"))}}),r=await run6(opts),used={role:opts.role,step:levelStep,level:lv.level,model:lv.model||tool5.modelFor(lv.level)};recordUsage(this.bp,key,used,r.usage);let asked=opts.resume??opts.sessionId;key&&asked&&r.sessionId&&r.sessionId!==asked&&this.update(key,x=>{for(let k of Object.keys(x.sessions??{}))x.sessions?.[k]===asked&&(x.sessions[k]=r.sessionId);x.chat?.side===asked&&(x.chat.side=r.sessionId),x.notesSeen?.[asked]!==void 0&&(x.notesSeen[r.sessionId]=x.notesSeen[asked])});let capped=res=>new CapReached(opts.role,levelStep,res.capped.by,caps,quality,AGENTS.find(a=>a.role===opts.role)?.name??opts.role,{sessionId:res.sessionId,...res.usage?{usage:res.usage}:{}}),who4=AGENTS.find(a=>a.role===opts.role)?.name??opts.role,tooMany=()=>win?.tooMany?new StepError(tooManyWords(who4),win.refusalDetail()):win?.tooManyMistakes?new StepError(tooManyMistakesWords(who4),win.mistakeDetail()):null;if(tooMany())throw tooMany();if(r.capped)throw capped(r);if(!r.ok)throw this.runFailed(r,tool5,key,opts.role);raise&&key&&this.update(key,x=>delete x.capRaise);let problem2=opts.schema?answerProblem(opts.schema,r.structured)??opts.check?.(r.structured)??null:null,done=r;if(opts.schema&&problem2){key&&levelStep!=="chat"&&this.update(key,x=>addActivity(x,opts.role,`The answer was not complete (${problem2}), so squad-kit asked for it again`));let again2=await run6(r.sessionId?{...opts,sessionId:void 0,resume:r.sessionId,prompt:answerAgainPrompt(problem2,opts.checkHelp?.())}:opts);if(recordUsage(this.bp,key,used,again2.usage),tooMany())throw tooMany();if(again2.capped)throw capped(again2);if(!again2.ok)throw this.runFailed(again2,tool5,key,opts.role);let broken=answerProblem(opts.schema,again2.structured);if(broken)throw new StepError(`The ${who4} did not hand in a complete answer.`,broken);done=again2}let settles=opts.role==="builder"?q4=>!!inTheWay(askText(q4)):void 0;for(let back=0;;){let found2=opts.schema?stoppedQuestion(done.structured,settles):null;if(!found2)return ended(opts.reply||opts.schema===CHAT_SCHEMA?this.plainReply(done,who4,opts.role,key):done);let say=text3=>key&&this.update(key,x=>addActivity(x,opts.role,text3));if(found2.verdict.kind==="not-allowed"){let line3=refusedAskLine(who4,found2.verdict.what);if(win)win.refuse("question",`${found2.verdict.why}: "${found2.q.text.slice(0,200)}"`,line3);else{if(say(line3),this.log("warn",`Refused the ${who4}'s question in ${job??"a run"}: ${found2.verdict.why}: "${found2.q.text.slice(0,200)}"`,key),back>=2)throw new StepError(tooManyWords(who4),`${found2.verdict.why}
@@ -1728,8 +1728,8 @@ ${specPrompt(t0,change,!1,others)}`,permissions:["read"],schema:SPEC_SCHEMA,time
1728
1728
  ${ask2}`,permissions:["read"],schema:design?DESIGN_SCHEMA:PLAN_SCHEMA,check:a=>dependsProblem(a,others.map(o=>o.id),id2),timeoutMs:WORDS_STEP_MS,addDirs:[...attachmentDirs(t0),...brd?.dirs??[]],onTool:this.watcher(id2,"architect",root)})).structured;design&&this.askIfNeeded(id2,"architect",out18.question);let mockup=out18.mockup?.trim()&&out18.screens>0?this.saveMockup(id2,out18.mockup):void 0,meta=this.meta(),named=meta.repos?.length?(out18.parts??[]).map(p=>String(p).trim()):[],inRepos=reposOfParts(meta,named),repo=meta.repos?.length?inRepos[0]??meta.repos.find(r2=>r2.path===out18.repo?.replace(/\/$/,""))?.path??meta.repos[0].path:void 0,held4=holdTestCommand((repo&&meta.repos?meta.repos.find(r2=>r2.path===repo)?.commands:meta.commands)??{},clean3(out18.commands??{}));Object.values(held4.apply).some(Boolean)&&(repo&&meta.repos?meta.repos=meta.repos.map(r2=>r2.path===repo?{...r2,commands:{...r2.commands,...held4.apply}}:r2):meta.commands={...meta.commands??{},...held4.apply},writeMeta(this.bp,meta)),held4.change&&this.log("info",`The design changes the test command from "${held4.change.from}" to "${held4.change.to}"`,id2),this.update(id2,t=>{repo&&!t.worktree&&(t.repo=repo),inRepos.length&&(t.parts=named.filter(p=>partsOf(meta).some(x=>x.id===p&&!x.shared&&!x.off))),held4.suggest&&(t.testSuggestion=held4.suggest),held4.change?(t.testChange={...held4.change,...repo?{repo}:{},at:now()},addActivity(t,"architect","The design changes how the tests run. squad-kit tries the new way after the build")):delete t.testChange,design||(t.story=out18.story,t.criteria=out18.criteria,t.releaseNote=out18.releaseNote),t.agentQuestion=void 0,mockup?(t.mockup=mockup,addActivity(t,"architect","Made a mockup of how the screen will look")):out18.screens||delete t.mockup,out18.decision?t.decision={...out18.decision}:delete t.decision;let known=new Set(listDecisions(this.bp).map(d=>d.id));if(t.follows=[...new Set(out18.follows??[])].filter(x=>known.has(x)),t.dependsOn=designDepends(this.bp,t,out18.dependsOn),t.decision?.replaces&&!known.has(t.decision.replaces)&&delete t.decision.replaces,t.steps=out18.steps.map(s=>({title:s,status:"todo"})),applyPlanEdits(t),t.work={steps:out18.steps.length,tests:Math.max(0,out18.tests),screens:Math.max(0,out18.screens),...out18.asks?{asks:Math.max(0,out18.asks)}:{}},addActivity(t,design?"architect":"spec",change?`Updated the ${design?"design":"plan"}: ${changeSummary(change)}`:design?"Wrote the design":"Wrote the plan"),design&&addDecided(t,"architect",out18.decided),(t.track==="express"||t.track==="auto")&&!change){t.status=t.track==="auto"?APPROVING:"Express track: approving the plan",scheduleSquad(t,"express-approve",.2,1);return}t.live=!1,t.status=design?"Design ready for you":"Plan ready for you",t.waiting={kind:"plan",summary:design?"The design is ready: is this how to build it?":"The plan is ready for your approval"},t.squad=null})}async workspace(id2){let root=projectRoot(this.bp),meta=this.meta(),t=readTicket(this.bp,id2),repo=repoFor(meta,t);if(t.worktree&&fs124.existsSync(t.worktree)){this.mendCopy(t),rewireStoryDb(this.bp,id2,t.worktree,repo.path);for(let c of t.copies??[])rewireStoryDb(this.bp,id2,c.dir,c.repo);this.placeLocalFiles(id2,t.worktree,repo.path);for(let c of t.copies??[])this.placeLocalFiles(id2,c.dir,c.repo);for(let owed of t.setupOwed??[]){let dir=owed===repo.path?t.worktree:t.copies?.find(c=>c.repo===owed)?.dir,of=owed===repo.path?repo:reposOf(meta).find(r=>r.path===owed);this.update(id2,x=>x.setupOwed=x.setupOwed?.filter(p=>p!==owed)),dir&&of&&await this.setUpCopy(id2,dir,of)}return this.addWantedCopies(id2)}let dep=dependencyToStartFrom(this.bp,t),at3=meta.repos?.length?storyCopyDir(root,id2,repo.path):void 0,fetched=dep?void 0:await fetchBase(this.bp,t),wt=await this.makingCopy(id2,how=>dep?addWorktreeOn(this.bp,t,dep,at3,how):addWorktree(path116.join(root,repo.path),id2,repo.base,fetched,at3,how)),named=repo.path==="."?"the project":repoLabel(meta,repo.path)??repo.path;return t=this.update(id2,x=>{x.worktree=wt.dir,at3&&(x.storyDir=path116.dirname(at3)),x.branch=wt.branch,x.baseCommit=x.baseCommit??wt.baseCommit,addActivity(x,"builder",wt.from?`Made a copy of ${named} from ${wt.from}`:wt.pushed?`Made a copy of ${named} from the pushed branch ${wt.pushed}, with the work so far`:`Made a clean copy of ${named}`)}),await commitTicketDocs(this.bp,t,wt.pushed?"picked up from the pushed branch":t.specApproved?"spec and design approved":"plan approved")&&(t=this.update(id2,x=>addActivity(x,"squad","Saved the spec and design"))),await this.setUpCopy(id2,wt.dir,repo),this.addWantedCopies(id2)}async copyForOutside(id2,repoPath){return await this.workspace(id2),repoPath&&await this.addCopy(id2,repoPath),readTicket(this.bp,id2)}async addWantedCopies(id2){let t=readTicket(this.bp,id2);if(t.storyDir)for(let repo of reposOfParts(this.meta(),t.parts))await this.addCopy(id2,repo);return readTicket(this.bp,id2)}async addCopy(id2,repoPath){let t=readTicket(this.bp,id2);if(!t.storyDir||repoFor(this.meta(),t).path===repoPath||t.copies?.some(c=>c.repo===repoPath))return;let repo=reposOf(this.meta()).find(r=>r.path===repoPath);if(!repo)return;let fetched=await fetchBase(this.bp,{...t,repo:repo.path}),wt=await this.makingCopy(id2,how=>addWorktree(path116.join(projectRoot(this.bp),repo.path),id2,repo.base,fetched,path116.join(t.storyDir,repo.path),how));this.update(id2,x=>{x.copies=[...x.copies??[],{repo:repo.path,dir:wt.dir,baseCommit:wt.baseCommit}],addActivity(x,"builder",`Made a clean copy of ${repoLabel(this.meta(),repo.path)??repo.path}`)}),await this.setUpCopy(id2,wt.dir,repo)}async setUpCopy(id2,dir,repo){let meta=this.meta();await this.storyDatabase(id2,dir,repo.path),this.placeLocalFiles(id2,dir,repo.path);let setup=repo.commands.setup?.trim();if(!setup)return;this.update(id2,x=>x.status=DOING.setup),this.log("info",`Setting up the copy: ${setup}`,id2);let r=await runCommand(dir,setup,SETUP_MS,this.guardedWatch(id2,dir));if(this.goOn(id2),this.placeLocalFiles(id2,dir,repo.path),!r.ok&&!meta.scan?.empty){let docker=dockerBlockedWords(r.output,this.guardFor(dir,id2));if(docker)throw this.update(id2,x=>x.setupOwed=[...new Set([...x.setupOwed??[],repo.path])]),new StepError(docker,`${setup}:
1729
1729
  ${tail(r.output,30)}`);this.update(id2,x=>{let before=x.setupTrouble,said=repo.path==="."?setup:`in ${repo.path}: ${setup}`;x.setupTrouble=before?{command:`${before.command}, and ${said}`,output:`${before.output}
1730
1730
 
1731
- ${tail(r.output,30)}`}:{command:meta.repos?.length?said:setup,output:tail(r.output,30)},addActivity(x,"squad","Setting up the copy did not work on this computer. The Builder sorts that out first.")})}let back=await putBackTracked(dir);if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),r.ok){let print=await setupPrintNow(dir);this.update(id2,x=>x.setupFor={...x.setupFor,[repo.path]:print})}}async setUpAgain(id2,c){let setup=c.repo.commands.setup?.trim();if(!setup||!fs124.existsSync(c.dir)||readTicket(this.bp,id2)?.setupFor?.[c.repo.path]===await setupPrintNow(c.dir))return null;this.update(id2,x=>addActivity(x,"squad","New packages or database changes came into this copy, so squad-kit sets it up again")),this.log("info",`Setting up the copy again: ${setup}`,id2);let theirs=await changedTracked(c.dir),run6=async()=>{let r2=await runCommand(c.dir,setup,SETUP_MS,this.guardedWatch(id2,c.dir));return this.placeLocalFiles(id2,c.dir,c.repo.path),r2},r=await run6();!r.ok&&DB_TROUBLE.test(r.output)&&!this.stops.get(id2)?.signal.aborted&&(await remakeStoryDb(this.bp,id2,c.dir,c.repo.path)||setAsideCopyDb(c.dir).length>0)&&(this.log("info",`The setup failed, so squad-kit made the copy's own database again and ran it once more. It said:
1732
- ${tail(r.output,15)}`,id2),this.update(id2,x=>addActivity(x,"squad","The copy's own database was out of step with the code, so squad-kit made it again")),r=await run6());let back=await putBackTracked(c.dir,{keep:theirs});if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),!r.ok)return{name:"setup",command:setup,ok:!1,output:r.output,seconds:r.seconds,...r.ended?{ended:!0}:{}};let print=await setupPrintNow(c.dir);return this.update(id2,x=>x.setupFor={...x.setupFor,[c.repo.path]:print}),null}async addPart(id2,which,lanes){let meta=this.meta(),part=partsOf(meta).find(p=>p.id===which.trim()||p.name.toLowerCase()===which.trim().toLowerCase());if(!part)return`There is no part "${which}". The parts: ${partsOf(meta).filter(p=>!p.shared&&!p.off).map(p=>p.id).join(", ")}.`;if(part.shared||!part.repo)return`${part.name} is a service the parts share, with no code of its own here.`;if(part.off)return`An engineer marked ${part.name} as not used. If the story needs it, say so in notes.`;let t=readTicket(this.bp,id2);if(!copiesOf(meta,t).some(c=>c.repo.path===part.repo)){await this.addCopy(id2,part.repo);let made=readTicket(this.bp,id2).copies?.find(c=>c.repo===part.repo);if(!made)return`squad-kit could not add a copy of ${part.name}. Call i_am_stuck if the story cannot go on without it.`;lanes?.push(await watchLane(made.dir,this.protectedFiles(made.dir))),this.update(id2,x=>addActivity(x,"squad",`The Builder needed ${part.name} too, so squad-kit added its copy`))}return this.update(id2,x=>{x.parts?.includes(part.id)||(x.parts=[...x.parts??[],part.id])}),`${part.name} is in the folder ${part.repo}/ of this story's folder. Its checks: run_checks with part "${part.id}".`}async writeChanges(t){if(t.storyDir)for(let c of copiesOf(this.meta(),t))await writeChangesOf(c.dir,c.baseCommit,path116.join(t.storyDir,changesFile(c.repo.path)),c.repo.path)}async storyDatabase(id2,dir,repo){let r=await setUpStoryDb(this.bp,id2,dir,repo).catch(err2=>(this.log("warn",`The story's own database was not set up: ${err2.message}`,id2),null));r?.state==="own"&&(r.made&&this.log("info",`Made ${r.name}, the story's own database`,id2),readTicket(this.bp,id2)?.activity.some(a=>a.text===OWN_DB_WORDS)||this.update(id2,x=>addActivity(x,"squad",OWN_DB_WORDS)))}placeLocalFiles(id2,dir,repo){let{missing:missing2}=placeStoryFiles(this.bp,repo,dir);if(!missing2.length)return;let line3=`An engineer has not given stories their values for ${missing2.join(", ")} yet, so the app may not start here.`;readTicket(this.bp,id2)?.activity.some(a=>a.text===line3)||this.update(id2,x=>addActivity(x,"squad",line3))}async pickedUp(id2){let t=await this.workspace(id2),next=carryOn(t,!0);this.update(id2,x=>{x.status=next.status,x.waiting=next.waiting??null,x.live=!!next.phase,next.phase?scheduleSquad(x,next.phase,.2,1):x.squad=null})}async build(id2){if(holdBuild(this.bp,id2))return;let t0=await this.workspace(id2);if(this.goOn(id2),t0.replanFor)return this.rebuild(id2,t0.replanFor);let meta=this.meta(),rc=(this.appOf(t0)?.repo??repoFor(meta,t0)).commands,sofar=t0.baseCommit?await workSoFar(t0.worktree,t0.baseCommit):[];!sofar.length&&!t0.proof?.length&&t0.baseCommit&&rc.start&&rc.url&&changesScreens(t0)&&!meta.scan?.empty&&!runsAsProduct(meta)&&this.shootBefore(id2);let had=t0.sessions?.build,t=this.update(id2,x=>{x.stage="implement",x.live=!0,x.status=`Step ${openStep(x)} of ${x.steps?.length??1}`,x.sessions={...x.sessions,build:x.sessions?.build??randomUUID7()},x.tries=0,x.reviewRound=0}),resume=!!had&&t.activity.some(a=>a.who==="builder"&&a.text.startsWith("Started")),cut=resume?carriesOn(t,"build"):void 0;this.update(id2,x=>addActivity(x,"builder",cut?carryOnLine(x,cut.by):resume?"Picked up where it stopped":sofar.length?"Started again from the spec, the design and the work so far":"Started building"));let base2=repoFor(meta,t).base,intro=`This ticket is partly built already: the work so far is in this copy, on ${t.branch}. It was done in another session, maybe on another computer, so you have no conversation about it. Files changed so far: ${sofar.slice(0,30).join(", ")}. Read the spec, the design and that code first (git log and git diff against ${base2} show it). Keep what is done and right, then finish the steps that are not done yet. squad-kit does all the git.`,plan=planKey(t),replanned=resume&&t.builtPlan!==void 0&&t.builtPlan!==plan,prompt=resume&&!replanned?cut?carryOnPrompt(t,cut.by):"Continue the steps that are not done yet, then run the tests.":`${replanned?`The plan changed since you last worked on this ticket. From now on follow the plan below, not the earlier one: keep the work that still fits it, change what does not, then finish the steps that are not done yet.
1731
+ ${tail(r.output,30)}`}:{command:meta.repos?.length?said:setup,output:tail(r.output,30)},addActivity(x,"squad","Setting up the copy did not work on this computer. The Builder sorts that out first.")})}let back=await putBackTracked(dir);if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),r.ok){let print=await setupPrintNow(dir);this.update(id2,x=>x.setupFor={...x.setupFor,[repo.path]:print})}}async setUpAgain(id2,c){let setup=c.repo.commands.setup?.trim();if(!setup||!fs124.existsSync(c.dir)||readTicket(this.bp,id2)?.setupFor?.[c.repo.path]===await setupPrintNow(c.dir))return{ran:!1};this.update(id2,x=>addActivity(x,"squad","New packages or database changes came into this copy, so squad-kit sets it up again")),this.log("info",`Setting up the copy again: ${setup}`,id2);let theirs=await changedTracked(c.dir),run6=async()=>{let r2=await runCommand(c.dir,setup,SETUP_MS,this.guardedWatch(id2,c.dir));return this.placeLocalFiles(id2,c.dir,c.repo.path),r2},r=await run6();!r.ok&&DB_TROUBLE.test(r.output)&&!this.stops.get(id2)?.signal.aborted&&(await remakeStoryDb(this.bp,id2,c.dir,c.repo.path)||setAsideCopyDb(c.dir).length>0)&&(this.log("info",`The setup failed, so squad-kit made the copy's own database again and ran it once more. It said:
1732
+ ${tail(r.output,15)}`,id2),this.update(id2,x=>addActivity(x,"squad","The copy's own database was out of step with the code, so squad-kit made it again")),r=await run6());let back=await putBackTracked(c.dir,{keep:theirs});if(back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2),!r.ok)return{ran:!0,failed:{name:"setup",command:setup,ok:!1,output:r.output,seconds:r.seconds,...r.ended?{ended:!0}:{}}};let print=await setupPrintNow(c.dir);return this.update(id2,x=>x.setupFor={...x.setupFor,[c.repo.path]:print}),{ran:!0}}async addPart(id2,which,lanes){let meta=this.meta(),part=partsOf(meta).find(p=>p.id===which.trim()||p.name.toLowerCase()===which.trim().toLowerCase());if(!part)return`There is no part "${which}". The parts: ${partsOf(meta).filter(p=>!p.shared&&!p.off).map(p=>p.id).join(", ")}.`;if(part.shared||!part.repo)return`${part.name} is a service the parts share, with no code of its own here.`;if(part.off)return`An engineer marked ${part.name} as not used. If the story needs it, say so in notes.`;let t=readTicket(this.bp,id2);if(!copiesOf(meta,t).some(c=>c.repo.path===part.repo)){await this.addCopy(id2,part.repo);let made=readTicket(this.bp,id2).copies?.find(c=>c.repo===part.repo);if(!made)return`squad-kit could not add a copy of ${part.name}. Call i_am_stuck if the story cannot go on without it.`;lanes?.push(await watchLane(made.dir,this.protectedFiles(made.dir))),this.update(id2,x=>addActivity(x,"squad",`The Builder needed ${part.name} too, so squad-kit added its copy`))}return this.update(id2,x=>{x.parts?.includes(part.id)||(x.parts=[...x.parts??[],part.id])}),`${part.name} is in the folder ${part.repo}/ of this story's folder. Its checks: run_checks with part "${part.id}".`}async writeChanges(t){if(t.storyDir)for(let c of copiesOf(this.meta(),t))await writeChangesOf(c.dir,c.baseCommit,path116.join(t.storyDir,changesFile(c.repo.path)),c.repo.path)}async storyDatabase(id2,dir,repo){let r=await setUpStoryDb(this.bp,id2,dir,repo).catch(err2=>(this.log("warn",`The story's own database was not set up: ${err2.message}`,id2),null));r?.state==="own"&&(r.made&&this.log("info",`Made ${r.name}, the story's own database`,id2),readTicket(this.bp,id2)?.activity.some(a=>a.text===OWN_DB_WORDS)||this.update(id2,x=>addActivity(x,"squad",OWN_DB_WORDS)))}placeLocalFiles(id2,dir,repo){let{missing:missing2}=placeStoryFiles(this.bp,repo,dir);if(!missing2.length)return;let line3=`An engineer has not given stories their values for ${missing2.join(", ")} yet, so the app may not start here.`;readTicket(this.bp,id2)?.activity.some(a=>a.text===line3)||this.update(id2,x=>addActivity(x,"squad",line3))}async pickedUp(id2){let t=await this.workspace(id2),next=carryOn(t,!0);this.update(id2,x=>{x.status=next.status,x.waiting=next.waiting??null,x.live=!!next.phase,next.phase?scheduleSquad(x,next.phase,.2,1):x.squad=null})}async build(id2){if(holdBuild(this.bp,id2))return;let t0=await this.workspace(id2);if(this.goOn(id2),t0.replanFor)return this.rebuild(id2,t0.replanFor);let meta=this.meta(),rc=(this.appOf(t0)?.repo??repoFor(meta,t0)).commands,sofar=t0.baseCommit?await workSoFar(t0.worktree,t0.baseCommit):[];!sofar.length&&!t0.proof?.length&&t0.baseCommit&&rc.start&&rc.url&&changesScreens(t0)&&!meta.scan?.empty&&!runsAsProduct(meta)&&this.shootBefore(id2);let had=t0.sessions?.build,t=this.update(id2,x=>{x.stage="implement",x.live=!0,x.status=`Step ${openStep(x)} of ${x.steps?.length??1}`,x.sessions={...x.sessions,build:x.sessions?.build??randomUUID7()},x.tries=0,x.reviewRound=0}),resume=!!had&&t.activity.some(a=>a.who==="builder"&&a.text.startsWith("Started")),cut=resume?carriesOn(t,"build"):void 0;this.update(id2,x=>addActivity(x,"builder",cut?carryOnLine(x,cut.by):resume?"Picked up where it stopped":sofar.length?"Started again from the spec, the design and the work so far":"Started building"));let base2=repoFor(meta,t).base,intro=`This ticket is partly built already: the work so far is in this copy, on ${t.branch}. It was done in another session, maybe on another computer, so you have no conversation about it. Files changed so far: ${sofar.slice(0,30).join(", ")}. Read the spec, the design and that code first (git log and git diff against ${base2} show it). Keep what is done and right, then finish the steps that are not done yet. squad-kit does all the git.`,plan=planKey(t),replanned=resume&&t.builtPlan!==void 0&&t.builtPlan!==plan,prompt=resume&&!replanned?cut?carryOnPrompt(t,cut.by):"Continue the steps that are not done yet, then run the tests.":`${replanned?`The plan changed since you last worked on this ticket. From now on follow the plan below, not the earlier one: keep the work that still fits it, change what does not, then finish the steps that are not done yet.
1733
1733
 
1734
1734
  `:sofar.length?`${intro}
1735
1735
 
@@ -1745,7 +1745,7 @@ ${prompt}`);let cwd=workFolder(t),started=Date.now(),current=-1,reached=n=>{n<=c
1745
1745
  `).filter(Boolean),merging=()=>git(dir,["rev-parse","-q","--verify","MERGE_HEAD"],{allowFail:!0})!=="",latest=latestBase(dir,base2,remote),left=merging()?await unmerged():[];if(left.length)return{baseSha:git(dir,["rev-parse","MERGE_HEAD"]),clashes:left,from:latest.ref};let baseSha=latest.sha;leaveProjectFiles(this.bp,t,base2);let guard=this.guardFor(dir,t.id),signal2=this.stops.get(t.id)?.signal,saved=await commitAll(dir,`Work so far on ${t.title} (${t.id})`,conventionalWords("chore",`work so far on ${t.title}`,t.id),{...guard?{guard}:{},...signal2?{signal:signal2}:{}});saved.hookSaid!==void 0&&(this.log("warn",`The project's own check said no to the work so far: ${saved.hookSaid}`,t.id),this.update(t.id,x=>{x.hookOwed=!0,addActivity(x,"squad","Saved the work so far without the project's own check, as it is not finished. The check runs on the whole change before it goes in")}));let why="git did not merge it";try{await gitLater(dir,["merge","--no-commit","--no-ff","-m",`Merge ${latest.ref} into ${t.branch??"this ticket"}`,baseSha])}catch(err2){why=err2.message}let clashes=await unmerged();if(!merging()&&!isAncestor(dir,baseSha,"HEAD"))throw new StepError("squad-kit could not bring the newest code into this ticket. Press Try again.",why);return{baseSha,clashes,from:latest.ref}}async bringInEveryBase(t){let copies=copiesOf(this.meta(),t),clashes=[],bases=new Map,first2;for(let c of copies){let r=await this.bringInBase(t,c.repo.base,await fetchBase(this.bp,{...t,repo:c.repo.path}),c.dir);first2??=r,clashes.push(...r.clashes.map(f=>t.storyDir?`${c.repo.path}/${f}`:f)),bases.set(c.repo.path,r.baseSha)}return t.copies?.length&&this.update(t.id,x=>x.copies=x.copies?.map(c=>({...c,baseCommit:bases.get(c.repo)??c.baseCommit}))),{...first2,clashes}}async anyBaseMoved(t){for(let c of copiesOf(this.meta(),t))if(this.baseMoved({...t,worktree:c.dir,baseCommit:c.baseCommit},c.repo.base,await fetchBase(this.bp,{...t,repo:c.repo.path})))return!0;return!1}async rebuild(id2,adr){let t=readTicket(this.bp,id2),base2=repoFor(this.meta(),t).base,cut=t.builtPlan===planKey(t)&&t.sessions?.build?carriesOn(t,"build"):void 0,moved=!cut||await this.anyBaseMoved(t),{baseSha,clashes,from}=moved?await this.bringInEveryBase(t):{baseSha:t.baseCommit,clashes:[],from:base2},t1=this.update(id2,x=>{x.baseCommit=baseSha,x.stage="implement",x.live=!0,x.tries=0,x.reviewRound=0,x.proof=void 0,x.gates={checks:"pending",review:"pending",proof:"pending"},x.sessions={...x.sessions,build:x.sessions?.build??randomUUID7()},x.status=cut?`Step ${openStep(x)} of ${x.steps?.length??1}`:`Redoing the work to follow ${adr}`,moved&&addActivity(x,"squad",`Brought the newest code into this ticket${clashes.length?`. Clashes in ${clashes.join(", ")}`:""}`),addActivity(x,"builder",cut?carryOnLine(x,cut.by):`Started redoing the work to follow ${adr}`)}),meta=this.meta();this.update(id2,x=>x.builtPlan=planKey(t1));let intro=`The plan changed to follow ${adr}, a decision approved after you started. The latest ${base2} is now in this copy${clashes.length?`, with clashes in ${clashes.join(", ")} to fix first`:""}. Stay on this branch: it already has the newest code, so a plan that says to start from ${base2} is met, and squad-kit does all the git. Redo the work to the new plan below: keep what still fits, change what does not.`,newer2=moved&&cut?` squad-kit brought the newest code into this copy meanwhile${clashes.length?`, with clashes in ${clashes.join(", ")} to fix first`:""}.`:"";await this.runBuilder(id2,cut?`${carryOnPrompt(t1,cut.by)}${newer2}`:`${intro}
1746
1746
 
1747
1747
  ${buildPrompt(meta,listDecisions(this.bp),t1,this.ctx(t1))}`,!!t.sessions?.build,"build"),this.update(id2,x=>{for(let s of x.steps??[])s.status="done",s.live=void 0;x.replanFor=void 0,x.progress=100,addActivity(x,"builder","Finished all steps"),x.stage="test",x.status="Running the project checks",scheduleSquad(x,"test",.2,1)})}baseMoved(t,base2,remote){try{let latest=latestBase(t.worktree,base2,remote).sha;return latest!==t.baseCommit&&!isAncestor(t.worktree,latest,"HEAD")}catch{return!0}}async updateBase(id2){let t=await this.workspace(id2),base2=repoFor(this.meta(),t).base,{baseSha,clashes,from}=await this.bringInEveryBase(t);this.update(id2,x=>{x.baseCommit=baseSha,x.stage="implement",x.live=!0,x.tries=0,x.reviewRound=0,x.proof=void 0,x.gates={checks:"pending",review:"pending",proof:"pending"},x.status=clashes.length?`Builder is fixing ${clashes.length} clashing ${clashes.length===1?"file":"files"}`:"Running the project checks",addActivity(x,"squad",`Brought the newest code into this ticket${clashes.length?`. Clashes in ${clashes.join(", ")}`:""}`)}),clashes.length&&(await this.runBuilder(id2,clashPrompt(base2,clashes),!!t.sessions?.build),this.update(id2,x=>addActivity(x,"builder",`Fixed the clash with ${base2}`))),this.update(id2,x=>{x.stage="test",x.status="Running the project checks",scheduleSquad(x,"test",.2,1)})}async fixFromPerson(id2){let t=await this.workspace(id2);this.update(id2,x=>{x.stage="implement",x.live=!0,x.status="Builder is fixing what you found",x.tries=0,x.fails=0,x.reviewRound=0,x.steps=[...x.steps??[],{title:`Fix: ${x.changeRequest?.split(`
1748
- `)[0]??"what you found"}`,status:"doing"}]}),await this.runBuilder(id2,changeFixPrompt(t.changeRequest??""),!!t.sessions?.build),this.update(id2,x=>{let last=x.steps?.[x.steps.length-1];last&&(last.status="done",last.live=void 0),addActivity(x,"builder","Fixed what you found"),x.changeRequest=void 0,x.stage="test",x.status="Running the project checks",x.proof=void 0,scheduleSquad(x,"test",.2,1)})}saveFoundCommands(id2,cwd,repoPath){let meta=this.meta(),t=readTicket(this.bp,id2),repo=(repoPath?reposOf(meta).find(r=>r.path===repoPath):void 0)??repoFor(meta,t),has3=k=>!!repo.commands[k]?.trim(),wanted=(!!meta.scan?.empty||!["test","lint","build","start","url"].some(has3)?["setup","test","lint","build","start","url"]:["test"]).filter(k=>!has3(k)&&!(k==="test"&&meta.noTests));if(!wanted.length||!fs124.existsSync(cwd))return repo.commands;let found2=findCommands(cwd),add2=Object.fromEntries(wanted.filter(k=>found2[k]?.trim()).map(k=>[k,found2[k].trim()]));return Object.keys(add2).length?(meta.repos?.length?meta.repos=meta.repos.map(r=>r.path===repo.path?{...r,commands:{...r.commands,...add2}}:r):meta.commands={...meta.commands??{},...add2},writeMeta(this.bp,meta),this.log("info",`Found and saved: ${Object.entries(add2).map(([k,v])=>`${k}: ${v}`).join(", ")}`,id2),this.update(id2,x=>{add2.test&&delete x.testSuggestion,addActivity(x,"squad",add2.test&&Object.keys(add2).length===1?"squad-kit found the tests in the project, and runs them from now on":"squad-kit found how to test and run the project, and saved it")}),{...repo.commands,...add2}):repo.commands}async builderPassed(id2,t,copies){let notes=this.builderRuns.get(id2);if(this.builderRuns.delete(id2),!notes?.size)return null;if(t.testChange&&!t.testChange.ask)return this.log("info","squad-kit runs the checks itself, not using the Builder's last run_checks: a new test command is still to be tried",id2),null;let out18=new Map;for(let c of copies){let here=notes.get(c.dir);if(!here?.size)continue;let tree=await treeOf(c.dir),taken=[];for(let w of checksToRun(c.dir,c.repo.commands)){let note2=here.get(w.name);if(!note2)continue;let why=note2.run.command!==w.command?`the checks now are not the ones it ran (${w.name}: ${w.command})`:note2.tree!==tree?`the code in ${c.label} changed since that run started`:"";why?this.log("info",`squad-kit runs ${w.name} itself, not using the Builder's last run_checks: ${why}`,id2):(taken.push(note2.run),this.log("info",`squad-kit uses the Builder's run_checks from ${new Date(note2.at).toISOString()} in ${c.label} for ${w.name} instead of running it again: it passed in the same copy, on the same code (tree ${note2.tree.slice(0,12)}): ${note2.run.command}`,id2))}taken.length&&out18.set(c,taken)}return out18.size?out18:null}async checks(id2){let t=readTicket(this.bp,id2);for(let c of copiesOf(this.meta(),t))this.saveFoundCommands(id2,c.dir,c.repo.path);let meta=this.meta(),copies=copiesOf(meta,t),several=copies.length>1,weak=[];for(let c of copies)c.baseCommit&&weak.push(...await weakenedChecks(c.dir,c.baseCommit,{skip:[`${path116.basename(this.bp.squadDir)}/`],testCommand:c.repo.commands.test}),...await gatesChanged(this.bp,c.dir,c.repo.path,c.baseCommit));let fresh=weak.filter(w=>!(t.weakenedContinued??[]).includes(w.key));if(fresh.length)return this.weakenedStop(id2,fresh,repoFor(meta,t).base);await this.clearBeforeApp(id2),this.goOn(id2);let setups=[];for(let c of copies){let failed3=await this.setUpAgain(id2,c);failed3&&setups.push(several?{...failed3,part:c.label}:failed3),this.goOn(id2)}let changed=[];for(let c of copies)(!several||c.baseCommit&&(await workSoFar(c.dir,c.baseCommit)).length)&&changed.push(c);let passed=await this.builderPassed(id2,t,changed.length?changed:copies.slice(0,1)),whole=(dir,commands,had)=>had.length>0&&checksToRun(dir,commands).every(w=>had.some(k=>k.name===w.name&&k.command===w.command))&&!startsToo(commands,had),reused=["test","lint","build"].filter(n=>[...passed?.values()??[]].some(ran3=>ran3.some(r=>r.name===n))),allReused=!!passed&&(changed.length?changed:copies.slice(0,1)).every(c=>whole(c.dir,c.repo.commands,passed?.get(c)??[]));this.update(id2,x=>{x.stage="test",x.live=!0,x.gates={checks:"pending",review:x.gates?.review??"pending",proof:"pending"},addActivity(x,"tester",allReused?CHECKS_PASSED_FOR_BUILDER:reused.length?someFromBuilder(reused):"squad-kit is running the project checks"),startStep(x,{who:"squad",step:"checks",limit:15})});let mark=sleepMark(),results=[...setups],ranIn=new Map;for(let c of changed.length?changed:copies.slice(0,1)){let commands=c.repo.commands,onStart=(name,command)=>{this.log("info",`Running ${name}: ${command??commands[name]}`,id2),this.update(id2,x=>{x.status=CHECK_DOING[name]??DOING.check,stepDoing(x,x.status)})},guard=this.guardFor(c.dir,id2),hookLane=await watchLane(c.dir,hookFiles(c.dir));(!t.testChange?.repo||t.testChange.repo===c.repo.path)&&(commands=await this.tryTestChange(id2,c.dir,commands,guard,mark));let had=passed?.get(c)??[],ran3=whole(c.dir,commands,had)?had:await runChecks2(c.dir,commands,onStart,{...this.commandWatch(id2),...guard?{guard}:{}},void 0,had),hooksBack=(await checkLane(hookLane)).restored;if(hooksBack.length&&this.update(id2,x=>addActivity(x,"squad",`The project's checks changed the check that runs on every save (${hooksBack.slice(0,6).join(", ")}); squad-kit put it back. Changing that check stays with you or an engineer`)),guard&&!this.stops.get(id2)?.signal.aborted){let hit=ran3.find(r=>!r.ok&&guardBlockedPath(r.output,guard));if(hit)return this.guardStop(id2,"test",guardBlockedPath(hit.output,guard),hit.command,hit.output,guard)}for(let r of ran3.map(x=>several?{...x,part:c.label}:x))results.push(r),ranIn.set(r,c);this.goOn(id2)}let gates=await runGatesAt2(this.bp,"checks",(changed.length?changed:copies.slice(0,1)).map(c=>({dir:c.dir,repo:c.repo.path,...several?{label:c.label}:{}})),t.addedSkipped??[],dir=>{let guard=this.guardFor(dir,id2);return{...this.commandWatch(id2),...guard?{guard}:{}}},name=>this.update(id2,x=>{x.status=`Running your team's gate: ${name}`,stepDoing(x,x.status)}));this.goOn(id2),this.update(id2,x=>noteGates(x,gates,text3=>addActivity(x,"tester",text3))),results.push(...gates.filter(g=>g.stops&&g.result!=="cant-run").map(g=>({name:PROJECT_CHECKS.includes(g.name.toLowerCase())?`${g.name} (your team's gate)`:g.name,command:g.command,ok:g.result==="passed",output:g.output,seconds:g.seconds,...g.failedFirst!==void 0?{failedFirst:g.failedFirst}:{},...g.label?{part:g.label}:{}})));let shown2=r=>r.part?`${r.part}: ${r.name}`:r.name;this.goOn(id2);let onMain=new Map;for(let r of results){if(r.ok||r.ended||r.name!=="test"||(t.checksContinued??[]).includes(shown2(r))||!ranIn.has(r))continue;let found2=await this.testsOnMain(id2,ranIn.get(r),r);this.goOn(id2),found2&&onMain.set(r,found2)}if(sleptSince(mark)){this.update(id2,x=>{delete x.clock,addActivity(x,"squad","The computer slept while the checks ran, so squad-kit runs them again. Not a failed try"),scheduleSquad(x,"test",.2,1)});return}this.update(id2,x=>endStep(this.bp,x,Date.now(),reused.length?{builderRun:{checks:reused,...allReused?{all:!0}:{}}}:{}));for(let r of results.filter(x=>x.failedFirst!==void 0))this.log("warn",`${shown2(r)} failed once and passed when run again (${r.command}):
1748
+ `)[0]??"what you found"}`,status:"doing"}]}),await this.runBuilder(id2,changeFixPrompt(t.changeRequest??""),!!t.sessions?.build),this.update(id2,x=>{let last=x.steps?.[x.steps.length-1];last&&(last.status="done",last.live=void 0),addActivity(x,"builder","Fixed what you found"),x.changeRequest=void 0,x.stage="test",x.status="Running the project checks",x.proof=void 0,scheduleSquad(x,"test",.2,1)})}saveFoundCommands(id2,cwd,repoPath){let meta=this.meta(),t=readTicket(this.bp,id2),repo=(repoPath?reposOf(meta).find(r=>r.path===repoPath):void 0)??repoFor(meta,t),has3=k=>!!repo.commands[k]?.trim(),wanted=(!!meta.scan?.empty||!["test","lint","build","start","url"].some(has3)?["setup","test","lint","build","start","url"]:["test"]).filter(k=>!has3(k)&&!(k==="test"&&meta.noTests));if(!wanted.length||!fs124.existsSync(cwd))return repo.commands;let found2=findCommands(cwd),add2=Object.fromEntries(wanted.filter(k=>found2[k]?.trim()).map(k=>[k,found2[k].trim()]));return Object.keys(add2).length?(meta.repos?.length?meta.repos=meta.repos.map(r=>r.path===repo.path?{...r,commands:{...r.commands,...add2}}:r):meta.commands={...meta.commands??{},...add2},writeMeta(this.bp,meta),this.log("info",`Found and saved: ${Object.entries(add2).map(([k,v])=>`${k}: ${v}`).join(", ")}`,id2),this.update(id2,x=>{add2.test&&delete x.testSuggestion,addActivity(x,"squad",add2.test&&Object.keys(add2).length===1?"squad-kit found the tests in the project, and runs them from now on":"squad-kit found how to test and run the project, and saved it")}),{...repo.commands,...add2}):repo.commands}async builderPassed(id2,t,copies){let notes=this.builderRuns.get(id2);if(this.builderRuns.delete(id2),!notes?.size)return null;if(t.testChange&&!t.testChange.ask)return this.log("info","squad-kit runs the checks itself, not using the Builder's last run_checks: a new test command is still to be tried",id2),null;let out18=new Map;for(let c of copies){let here=notes.get(c.dir);if(!here?.size)continue;let tree=await treeOf(c.dir),taken=[];for(let w of checksToRun(c.dir,c.repo.commands)){let note2=here.get(w.name);if(!note2)continue;let why=note2.run.command!==w.command?`the checks now are not the ones it ran (${w.name}: ${w.command})`:note2.tree!==tree?`the code in ${c.label} changed since that run started`:"";why?this.log("info",`squad-kit runs ${w.name} itself, not using the Builder's last run_checks: ${why}`,id2):(taken.push(note2.run),this.log("info",`squad-kit uses the Builder's run_checks from ${new Date(note2.at).toISOString()} in ${c.label} for ${w.name} instead of running it again: it passed in the same copy, on the same code (tree ${note2.tree.slice(0,12)}): ${note2.run.command}`,id2))}taken.length&&out18.set(c,taken)}return out18.size?out18:null}async checks(id2){let t=readTicket(this.bp,id2);for(let c of copiesOf(this.meta(),t))this.saveFoundCommands(id2,c.dir,c.repo.path);let meta=this.meta(),copies=copiesOf(meta,t),several=copies.length>1,weak=[];for(let c of copies)c.baseCommit&&weak.push(...await weakenedChecks(c.dir,c.baseCommit,{skip:[`${path116.basename(this.bp.squadDir)}/`],testCommand:c.repo.commands.test}),...await gatesChanged(this.bp,c.dir,c.repo.path,c.baseCommit));let fresh=weak.filter(w=>!(t.weakenedContinued??[]).includes(w.key));if(fresh.length)return this.weakenedStop(id2,fresh,repoFor(meta,t).base);await this.clearBeforeApp(id2),this.goOn(id2);let setups=[];for(let c of copies){let{failed:failed3}=await this.setUpAgain(id2,c);failed3&&setups.push(several?{...failed3,part:c.label}:failed3),this.goOn(id2)}let changed=[];for(let c of copies)(!several||c.baseCommit&&(await workSoFar(c.dir,c.baseCommit)).length)&&changed.push(c);let passed=await this.builderPassed(id2,t,changed.length?changed:copies.slice(0,1)),whole=(dir,commands,had)=>had.length>0&&checksToRun(dir,commands).every(w=>had.some(k=>k.name===w.name&&k.command===w.command))&&!startsToo(commands,had),reused=["test","lint","build"].filter(n=>[...passed?.values()??[]].some(ran3=>ran3.some(r=>r.name===n))),allReused=!!passed&&(changed.length?changed:copies.slice(0,1)).every(c=>whole(c.dir,c.repo.commands,passed?.get(c)??[]));this.update(id2,x=>{x.stage="test",x.live=!0,x.gates={checks:"pending",review:x.gates?.review??"pending",proof:"pending"},addActivity(x,"tester",allReused?CHECKS_PASSED_FOR_BUILDER:reused.length?someFromBuilder(reused):"squad-kit is running the project checks"),startStep(x,{who:"squad",step:"checks",limit:15})});let mark=sleepMark(),results=[...setups],ranIn=new Map;for(let c of changed.length?changed:copies.slice(0,1)){let commands=c.repo.commands,onStart=(name,command)=>{this.log("info",`Running ${name}: ${command??commands[name]}`,id2),this.update(id2,x=>{x.status=CHECK_DOING[name]??DOING.check,stepDoing(x,x.status)})},guard=this.guardFor(c.dir,id2),hookLane=await watchLane(c.dir,hookFiles(c.dir));(!t.testChange?.repo||t.testChange.repo===c.repo.path)&&(commands=await this.tryTestChange(id2,c.dir,commands,guard,mark));let had=passed?.get(c)??[],ran3=whole(c.dir,commands,had)?had:await runChecks2(c.dir,commands,onStart,{...this.commandWatch(id2),...guard?{guard}:{}},void 0,had),hooksBack=(await checkLane(hookLane)).restored;if(hooksBack.length&&this.update(id2,x=>addActivity(x,"squad",`The project's checks changed the check that runs on every save (${hooksBack.slice(0,6).join(", ")}); squad-kit put it back. Changing that check stays with you or an engineer`)),guard&&!this.stops.get(id2)?.signal.aborted){let hit=ran3.find(r=>!r.ok&&guardBlockedPath(r.output,guard));if(hit)return this.guardStop(id2,"test",guardBlockedPath(hit.output,guard),hit.command,hit.output,guard)}for(let r of ran3.map(x=>several?{...x,part:c.label}:x))results.push(r),ranIn.set(r,c);this.goOn(id2)}let gates=await runGatesAt2(this.bp,"checks",(changed.length?changed:copies.slice(0,1)).map(c=>({dir:c.dir,repo:c.repo.path,...several?{label:c.label}:{}})),t.addedSkipped??[],dir=>{let guard=this.guardFor(dir,id2);return{...this.commandWatch(id2),...guard?{guard}:{}}},name=>this.update(id2,x=>{x.status=`Running your team's gate: ${name}`,stepDoing(x,x.status)}));this.goOn(id2),this.update(id2,x=>noteGates(x,gates,text3=>addActivity(x,"tester",text3))),results.push(...gates.filter(g=>g.stops&&g.result!=="cant-run").map(g=>({name:PROJECT_CHECKS.includes(g.name.toLowerCase())?`${g.name} (your team's gate)`:g.name,command:g.command,ok:g.result==="passed",output:g.output,seconds:g.seconds,...g.failedFirst!==void 0?{failedFirst:g.failedFirst}:{},...g.label?{part:g.label}:{}})));let shown2=r=>r.part?`${r.part}: ${r.name}`:r.name;this.goOn(id2);let onMain=new Map;for(let r of results){if(r.ok||r.ended||r.name!=="test"||(t.checksContinued??[]).includes(shown2(r))||!ranIn.has(r))continue;let found2=await this.testsOnMain(id2,ranIn.get(r),r);this.goOn(id2),found2&&onMain.set(r,found2)}if(sleptSince(mark)){this.update(id2,x=>{delete x.clock,addActivity(x,"squad","The computer slept while the checks ran, so squad-kit runs them again. Not a failed try"),scheduleSquad(x,"test",.2,1)});return}this.update(id2,x=>endStep(this.bp,x,Date.now(),reused.length?{builderRun:{checks:reused,...allReused?{all:!0}:{}}}:{}));for(let r of results.filter(x=>x.failedFirst!==void 0))this.log("warn",`${shown2(r)} failed once and passed when run again (${r.command}):
1749
1749
  ${r.failedFirst}`,id2),this.update(id2,x=>addActivity(x,"tester",failedOnceLine(r.name)));let test=results.find(r=>r.name==="test");test?.ok&&test.failedFirst===void 0&&forgetKnownFailure(this.bp,t,test.command);let oldOnly=new Set([...onMain].filter(([,m])=>m.old.length&&!m.fine.length).map(([r])=>r)),failed2=results.filter(r=>!r.ok&&!oldOnly.has(r)),cant=gates.filter(g=>g.stops&&g.result==="cant-run"),old=[];for(let[r,m]of onMain){if(!m.old.length)continue;let fixes=storiesForOld(this.bp,readTicket(this.bp,id2),m.old,m.printed,{command:r.command,...meta.repos?.length?{repo:ranIn.get(r).repo.path}:{},...r.part?{part:r.part}:{}});this.log("info",`${shown2(r)} failed on main's code too, without ${id2}'s change (${r.command}): ${fixes.map(f=>`${f.file} \u203A ${f.title}${f.story?` (${f.story})`:""}`).join("; ")}`,id2);let waiting2=fixes.flatMap(f=>f.story&&readTicket(this.bp,f.story)?.waitsToStart?[f.story]:[]),line3=oldLine(fixes,!failed2.length&&!cant.length,waiting2);this.update(id2,x=>{x.activity.some(a=>a.text===line3)||addActivity(x,"tester",line3)}),oldOnly.has(r)&&old.push(...fixes)}if(cant.length&&!failed2.length)return this.gateCantRun(id2,cant);let ran2=ranOf(results),around=weak.length>0;if(failed2.length===0&&noTestsRan(results))return this.notChecked(id2,results,around);if(failed2.length===0){let tests=test?testCount(test.output):null,parts2=[...new Set(results.map(r=>r.part).filter(Boolean))],names2=parts2.length?parts2.map(p=>`${p} \u2713`).join(", "):results.map(r=>checkWords(r.name)).join(", "),passed2=parts2.length?names2:tests?`${tests} ${tests===1?"test passes":"tests pass"}, ${names2} OK`:`${names2} pass`,text3=old.length?`${passed2}; ${oldGateText(old.length)}`:passed2;this.update(id2,x=>{x.gates={...x.gates,checks:around?"skipped":"pass",checksText:around?`${text3}; tests weakened, you continued`:text3,ran:ran2,...old.length?{oldFailures:old}:{}},x.tries=0,clearStaleStop(x),x.checksContinued=void 0,addActivity(x,"tester",old.length?`All checks pass, apart from ${old.length===1?"the old test that fails":"the old tests that fail"} on main too: ${names2}`:`All checks pass: ${names2}`),x.stage="review",x.status="Reviewer is reading the changes",scheduleSquad(x,"review",.2,1,(x.reviewRound??0)+1)});for(let d of listDecisions(this.bp))d.from===id2&&d.checkedBy&&!d.checkedBy.passing&&writeDecision(this.bp,{...d,checkedBy:{...d.checkedBy,passing:!0}});return}let past=t.checksContinued;if(past?.length&&failed2.every(f=>past.includes(shown2(f)))){let names2=failed2.map(shown2).join(", ");this.update(id2,x=>{x.tries=0,x.gates={...x.gates,checks:"skipped",checksText:`${names2} still failed, continued anyway`,ran:ran2},clearStaleStop(x),addActivity(x,"tester",`Checks still fail: ${names2}. You continued anyway past them, so the work goes on`),x.stage="review",x.status="Reviewer is reading the changes",scheduleSquad(x,"review",.2,1,(x.reviewRound??0)+1)});return}let tries=(t.tries??0)+1;if(this.update(id2,x=>{x.tries=tries,x.fails=(x.fails??0)+1,x.gates={...x.gates,checks:"fail",checksText:`${failed2.map(shown2).join(", ")} failed`,ran:ran2},addActivity(x,"tester",`Checks failed: ${failed2.map(shown2).join(", ")} (try ${tries} of ${MAX_CHECK_TRIES})`)}),tries>=MAX_CHECK_TRIES){this.update(id2,x=>{x.live=!1,x.squad=null,x.retryPhase="test",x.status=`Stuck after ${tries} check runs`,x.stuck={gate:"checks",checks:failed2.map(shown2),question:`The checks still fail after ${tries} tries: ${checkNames(failed2.map(shown2))} did not pass.`,detail:failed2.map(f=>`${shown2(f)} (${f.command}):
1750
1750
  ${tail(f.output,30)}`).join(`
1751
1751
 
@@ -1755,7 +1755,8 @@ ${tail(f.output,30)}`).join(`
1755
1755
  `),this.log("warn",`Could not take screenshots: ${detail||why}`,id2),this.update(id2,x=>addActivity(x,"tester","Could not take the screenshots this time"))}let now2=readTicket(this.bp,id2),shot=(now2.proof??[]).some(p=>p.after);if(canShow&&(!shot||bad2.length)){this.update(id2,x=>{let named=n=>criterionText2(x,n)??`"Done when" ${n}`,missing2=round?missingShots(x,round):[],stuck=round?.stuck.length?round.stuck:[];x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof",x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"},x.status=bad2.length?"Screenshots did not pass the check":"No screenshots yet",!bad2.length&&!why&&addActivity(x,"tester","No screenshots yet, so it waits for you");let said=round?.said?` The Tester says: ${round.said}`:"",technical=stuck.length?stuck.map(s=>`${s.what}${s.why?`: ${s.why}`:""}`).join(`
1756
1756
  `):"";x.stuck=bad2.length?{gate:"proof",kind:"bad-proof",question:"The screenshots did not pass squad-kit's check, even after the Tester took them again, so they do not prove the work yet.",points:bad2.map(b=>b.text),options:wayOptions({gate:"proof",kind:"bad-proof"})}:{gate:"proof",question:why||(stuck.length?STUCK_PICTURES:`The Tester took no screenshots, so the proof is not ready yet.${said}`),...!why&&missing2.length?{points:missing2.map(m=>`${named(m.criterion)}: ${m.why}`)}:{},...detail||technical?{detail:[detail,technical].filter(Boolean).join(`
1757
1757
  `)}:{},options:wayOptions({gate:"proof"})},x.waiting={kind:"stuck",summary:bad2.length?"The screenshots did not pass the check: needs one decision":"No screenshots yet: needs one decision"}});return}if(canShow&&round&&doneWhenCount(now2)>0){let judged;try{judged=await this.judge(id2,round)}catch(err2){if(err2 instanceof LimitReached||err2 instanceof CapReached||err2 instanceof CutOff||err2 instanceof PassingTrouble||this.stops.get(id2)?.signal.aborted)throw err2;return this.checkStop(id2,err2)}if(judged.broken.length)return this.notWorking(id2,judged.broken,judged.notShown);if(judged.notShown.length)return this.cannotShow(id2,judged.notShown,!0,this.listNoes.get(id2));if(judged.offScreen.length===doneWhenCount(now2))return this.cannotShow(id2,judged.offScreen,!1,this.listNoes.get(id2))}if(this.listNoes.get(id2)?.length)return this.cannotShow(id2,[],!0,this.listNoes.get(id2));this.update(id2,x=>{x.proof=(x.proof??[]).filter(p=>p.after);let shots=x.proof;if(x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"pass"},x.proofRound=void 0,clearStaleStop(x),markCheckedOutside(x),!shots.length){let proof=x.gates?.checks==="unchecked"?"and no tests ran, so read the changes yourself":"so the checks are the proof";addActivity(x,"tester",canShow?`No screenshots were taken, ${proof}`:`No screens to show, ${proof}`)}if(x.track==="auto")return acceptNext(x,.2,1);x.live=!1,x.stage="review",x.status="Proof ready to check",x.waiting={kind:"proof",summary:"The proof is ready for you to check"},x.squad=null})}async judge(id2,first2){let sizes={...first2.sizes},rowOf=(x,n)=>(x.proof??[]).find(p=>p.after&&p.criterion===criterionText2(x,n)),reasonOf=(x,n)=>x.offScreen?.find(o=>o.criterion===criterionText2(x,n))?.why,t=readTicket(this.bp,id2),numbers=Array.from({length:doneWhenCount(t)},(_,i)=>i+1),toFile=p=>({file:p.after,...p.page?{page:p.page}:{},...sizes[p.after]?{size:sizes[p.after]}:{},...p.note?{note:p.note}:{}}),toCheck=(x,which)=>which.flatMap(n=>{let row3=rowOf(x,n),reason=reasonOf(x,n),off=reason?{offScreen:reason}:{};return row3?[{criterion:n,...toFile(row3),...off,...row3.shows?{said:SAID[row3.shows]}:{},...this.sameAsBefore(row3)?{same:!0}:{},...row3.more?.length?{more:row3.more.map(toFile)}:{}}]:reason?[{criterion:n,...off}]:[]}),verdicts=await this.checkPictures(id2,toCheck(t,numbers)),verdict=(x,n)=>{let row3=rowOf(x,n);if(row3)return together(row3,verdicts.get(n),this.sameAsBefore(row3));let reason=reasonOf(x,n);return reason?fromReason(reason,verdicts.get(n)):null},isOpen=(x,n)=>{let v=verdict(x,n);return!v||v.shows==="not-shown"||v.shows==="off-screen"&&!reasonOf(x,n)};for(let retake=1;retake<=2;retake+=1){let x=readTicket(this.bp,id2),open3=numbers.filter(n=>isOpen(x,n)),noes=this.listNoes.get(id2)??[];if(!open3.length&&!noes.length)break;let asks=[...noes.map(no=>`Your team's checklist "${no.list}" asks: ${no.question} You answered no${no.why?` (${no.why})`:""}. Look again, show it if you can, and answer this question again in "checklist".`),...open3.map(n=>{let v=verdict(x,n),text3=criterionText2(x,n),reason=reasonOf(x,n);return v?v.shows==="off-screen"?cannotShowLine(n,text3,v.why):reason?reasonRefusedLine(n,text3,reason,v.why,v.advice):retakeLine(n,text3,v.why,v.advice):`"Done when" ${n} ("${text3}") has no screenshot, and you did not say it shows on no screen. Take one, or put it in offScreen with why.`})],which=open3.length===1?`"done when" ${open3[0]}`:open3.length?`${open3.length} "done when"`:"your team's checklist";this.update(id2,y2=>addActivity(y2,"tester",open3.length?`The screenshots do not show ${which} clearly yet, so the Tester takes ${open3.length===1?"it":"them"} again (try ${retake} of ${2})`:`The Tester answered no on ${which}, so it looks again (try ${retake} of ${2})`));let had=(z13,n)=>`${rowOf(z13,n)?rowPictures(rowOf(z13,n)).map(p=>p.after).sort().join(" "):""}|${reasonOf(z13,n)??""}`,before=new Map(numbers.map(n=>[n,had(x,n)])),again2=await this.screens(id2,"after",void 0,asks);Object.assign(sizes,again2.sizes);let y=readTicket(this.bp,id2),fresh=[...before].filter(([n,seen])=>had(y,n)!==seen).map(([n])=>n);for(let[n,v]of await this.checkPictures(id2,toCheck(y,fresh)))verdicts.set(n,v)}let out18={broken:[],notShown:[],offScreen:[]};return this.update(id2,x=>{let stays=[];for(let n of numbers){let row3=rowOf(x,n),text3=criterionText2(x,n),v=row3?afterLastRetake(verdict(x,n),reasonOf(x,n)):verdict(x,n),said=v?plainParts(v.why,technicalWords).kept:"";if(!row3){if(v?.shows==="off-screen"){let why=(said||reasonOf(x,n)).slice(0,600);stays.push({criterion:text3,why}),out18.offScreen.push({criterion:n,text:text3,why})}else out18.notShown.push({criterion:n,text:text3,why:(said||"No screenshot shows it.").slice(0,600)});continue}if(row3.shows=v.shows,v.shows==="met"){delete row3.why;continue}row3.why=(said||(v.shows==="broken"?"The screenshot shows it does not work as written.":v.shows==="off-screen"?"The screenshots show a part of it. The rest cannot be shown on a screen.":"The screenshot does not show it clearly.")).slice(0,600),(v.shows==="broken"?out18.broken:v.shows==="off-screen"?out18.offScreen:out18.notShown).push({criterion:n,text:text3,why:row3.why})}stays.length?x.offScreen=stays:delete x.offScreen;let met=(x.proof??[]).filter(p=>p.shows==="met").length,open3=[out18.broken.length?`${out18.broken.length} not working yet`:"",out18.notShown.length?`${out18.notShown.length} not shown yet`:"",out18.offScreen.length?`${out18.offScreen.length} not on a screen`:""].filter(Boolean);addActivity(x,"reviewer",open3.length?`Checked the screenshots: ${[`${met} met`,...open3].join(", ")}`:'Checked the screenshots: each "done when" on a screen is met');for(let r of out18.offScreen)addActivity(x,"reviewer",`"Done when" ${r.criterion} is not on a screen: ${r.why}`)}),out18}sameAsBefore(p){if(p.beforeLabel!=="Before"||!p.before||!p.after||p.before===p.after)return!1;try{return samePicture(fs124.readFileSync(path116.join(this.bp.proofDir,p.before)),fs124.readFileSync(path116.join(this.bp.proofDir,p.after)))}catch{return!1}}pairWithBefore(row3){if(!row3.more?.length||!this.sameAsBefore(row3))return;let at3=row3.more.findIndex(m=>!this.sameAsBefore({...row3,after:m.after}));at3>=0&&makeMain(row3,at3)}beforeSizes(t){let rows2=(t.proof??[]).filter(p=>p.before&&p.beforeLabel!=="New screen");return Object.fromEntries(rows2.flatMap(p=>{let size2=shotSizeOf(path116.join(this.bp.proofDir,p.before));return size2?[[p.criterion,size2]]:[]}))}sameFile(a,b){if(a===b)return!0;try{return fs124.readFileSync(path116.join(this.bp.proofDir,a)).equals(fs124.readFileSync(path116.join(this.bp.proofDir,b)))}catch{return!1}}async checkPictures(id2,pictures){if(!pictures.length)return new Map;let t=this.update(id2,x=>{x.status="Reviewer is checking the screenshots",x.live=!0}),asked=pictures.map(p=>p.criterion),full=file3=>path116.join(this.bp.proofDir,file3),r=await this.runAgent({role:"reviewer",step:"proof-check",levelStep:"review",ticket:id2,cwd:workFolder(t),sessionId:randomUUID7(),prompt:proofCheckPrompt(t,pictures.map(p=>({...p,...p.file?{file:full(p.file)}:{},...p.more?{more:p.more.map(m=>({...m,file:full(m.file)}))}:{}}))),addDirs:[...this.extraDirs(t),this.bp.proofDir],permissions:["read"],schema:PROOF_CHECK_SCHEMA,timeoutMs:REVIEW_STEP_MS,check:answer2=>{let left=asked.filter(n=>!checkVerdicts(answer2,asked,!0).has(n));return left.length?`checks has no verdict for "done when" ${left.join(", ")}`:null},onTool:this.watcher(id2,"reviewer",workFolder(t))});return checkVerdicts(r.structured,asked)}checkStop(id2,err2){let e=explainError(err2);this.log("warn",`Could not check the screenshots: ${[e.what,e.detail].filter(Boolean).join(": ")}`,id2),this.update(id2,x=>{x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof",x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"},x.status="The screenshots are not checked yet",addActivity(x,"reviewer","Could not check the screenshots this time"),x.stuck={gate:"proof",question:ownWords2(err2)??CHECK_FAILED,detail:[e.what,e.detail].filter(Boolean).join(`
1758
- `),options:wayOptions({gate:"proof"})},x.waiting={kind:"stuck",summary:"The screenshots are not checked yet: needs one decision"}})}async notWorking(id2,rows2,notShown){let round=this.update(id2,x=>{x.proofRound=(x.proofRound??0)+1,x.fails=(x.fails??0)+1,x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"}}).proofRound;if(round>=2){let stop=brokenStop(rows2);this.update(id2,x=>{x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof-fix",x.status="The screenshots show it is not working yet",addActivity(x,"tester",`Still not working after ${round} rounds: ${rows2.map(r=>`"done when" ${r.criterion}`).join(", ")}`),x.stuck={gate:"proof",kind:"broken",question:stop.question,points:[...stop.points,...notShown.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)],options:wayOptions({gate:"proof",kind:"broken"})},x.waiting={kind:"stuck",summary:"The screenshots show it is not working yet: needs one decision"}});return}this.goOn(id2),this.update(id2,x=>{addActivity(x,"tester",`The screenshots show ${rows2.length===1?'one "done when"':`${rows2.length} "done when"`} not working yet, so the Builder fixes ${rows2.length===1?"it":"them"}: ${rows2.map(r=>`"done when" ${r.criterion}: ${r.why}`).join(" ")}`),x.stage="implement",x.live=!0,x.status="Builder is fixing what the screenshots show"}),await this.runBuilder(id2,proofFixPrompt(rows2.map(fixLine),!readTicket(this.bp,id2)?.noNewTests),!0),this.afterProofFix(id2)}cannotShow(id2,rows2,retook=!0,noes=[]){let stop=rows2.length?notShownStop(rows2):{question:"The Tester still answers no on your team's checklist.",points:[]},listWords2=noes.map(n=>`Your team's checklist "${n.list}": ${n.question}${n.why?` (${n.why})`:""}`);this.update(id2,x=>{x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof",x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"};let listOnly=!rows2.length;x.status=listOnly?"A no on your team's checklist":"The screenshots do not show it yet",addActivity(x,"tester",listOnly?`The Tester still answers no on your team's checklist after ${2} more tries`:retook?`Could not show ${rows2.map(r=>`"done when" ${r.criterion}`).join(", ")} on a screen after ${2} more tries`:'Every "done when" ended as not on a screen, so the screenshots do not show the work'),x.stuck={gate:"proof",kind:"not-shown",question:stop.question,points:[...stop.points,...listWords2],options:wayOptions({gate:"proof",kind:"not-shown"})},x.waiting={kind:"stuck",summary:`${x.status}: needs one decision`}})}async proofFix(id2){let t=await this.workspace(id2),rows2=brokenRows(t);if(!rows2.length)return this.proof(id2);this.update(id2,x=>{x.stage="implement",x.live=!0,x.status="Builder is fixing what the screenshots show"}),await this.runBuilder(id2,proofFixPrompt(rows2.map(fixLine),!t.noNewTests),!!t.sessions?.build),this.afterProofFix(id2)}afterProofFix(id2){this.update(id2,x=>{addActivity(x,"builder","Fixed what the screenshots showed"),x.reviewRound=0,x.stage="test",x.status="Running the project checks",scheduleSquad(x,"test",.2,1)})}shootBefore(id2){let key=`__before:${id2}`;if(this.running.has(key))return;let ended=()=>{};this.beforeShotsOf.set(id2,new Promise(resolve2=>ended=resolve2)),this.launch(key,()=>this.beforeShots(id2,key).finally(()=>{this.beforeShotsOf.delete(id2),ended()}))}async beforeShots(id2,key){let own2=this.stops.get(key),cutOff=!1,timer=setTimeout(()=>{cutOff=!0,own2.abort()},BEFORE_SHOTS_MS),t=readTicket(this.bp,id2),repo=appCopyOf(this.meta(),t)?.repo??repoFor(this.meta(),t),root=path116.join(projectRoot(this.bp),repo.path),dir=null;try{dir=await addSnapshot(root,`${id2.toLowerCase()}-before`,t.baseCommit,{signal:own2.signal});let setup=repo.commands.setup?.trim();if(setup){let r=await runCommand(dir,setup,BEFORE_SHOTS_MS,{signal:own2.signal,onHealed:detail=>this.healed(id2,detail),...this.guardFor(dir,id2)?{guard:this.guardFor(dir,id2)}:{}});if(own2.signal.aborted)throw new Error(STOPPED10);if(!r.ok)throw new Error(`setting up a copy of the code as it was failed (${setup}): ${tail(r.output,3)}`)}let left=(await this.screens(id2,"before",{cwd:dir,job:key,signal:own2.signal})).problems;left.length&&this.update(id2,x=>addActivity(x,"tester",`Left out before screenshots that did not pass squad-kit's check: ${left.map(b=>b.text).join(" ")}`))}catch(err2){if(cutOff||!own2.signal.aborted){let e=explainError(err2);cutOff||this.log("warn",`Could not take the before screenshots: ${[e.what,e.detail].filter(Boolean).join(": ")}`,id2),this.update(id2,x=>addActivity(x,"tester",cutOff?"Stopped the before screenshots after 8 minutes, so the proof shows the screens after the change only":"Could not take the before screenshots, so the proof shows the screens after the change only"))}}finally{clearTimeout(timer),dir&&(await runsEnded([dir],6e3),await this.clearCopies([dir],{},id2).catch(()=>{}),await dropSnapshot(root,dir))}}async testsOnMain(id2,c,r){if(!c.baseCommit)return null;let tests=testsToTry(r.output,c.dir,r.command,await workSoFar(c.dir,c.baseCommit));if(!tests)return null;let t=readTicket(this.bp,id2);if(tests.some(x=>t?.oldTests?.includes(testKey(x))))return null;let seen=t?.onMain?.find(m=>m.repo===c.repo.path&&m.base===c.baseCommit),found2={old:tests.filter(x=>seen?.old.includes(testKey(x))),fine:tests.filter(x=>seen?.fine.includes(testKey(x))),printed:{}},left=tests.filter(x=>!found2.old.includes(x)&&!found2.fine.includes(x));if(!left.length)return found2;let fresh=await this.inMainCopy(id2,c,(dir,watch)=>runOnMain(dir,r.command,left,(one2,ms2)=>runCommand(dir,one2,ms2,{silentMs:CHECK_SILENT_MS,...watch}),Date.now()+ON_MAIN_MS));return fresh?(found2.old.push(...fresh.old),found2.fine.push(...fresh.fine),Object.assign(found2.printed,fresh.printed),this.log("info",`Ran the failed tests on main's code (${c.baseCommit.slice(0,8)}): ${[...fresh.old.map(x=>`failed there too: ${testKey(x)}`),...fresh.fine.map(x=>`passed there: ${testKey(x)}`)].join("; ")}`,id2),this.update(id2,x=>{let rest=(x.onMain??[]).filter(m=>m.repo!==c.repo.path),keys=list2=>[...new Set(list2.map(testKey))];x.onMain=[...rest,{repo:c.repo.path,base:c.baseCommit,old:keys(found2.old),fine:keys(found2.fine)}]}),found2):null}async inMainCopy(id2,c,use){await this.beforeShotsOf.get(id2),this.goOn(id2);let root=path116.join(projectRoot(this.bp),c.repo.path),stop=this.stops.get(id2)?.signal,within2=ms2=>stop?AbortSignal.any([stop,AbortSignal.timeout(ms2)]):AbortSignal.timeout(ms2),said=readTicket(this.bp,id2)?.status,doing=text3=>this.update(id2,x=>{x.status=text3,stepDoing(x,text3)}),dir=null;try{let ready=within2(BEFORE_SHOTS_MS);dir=await this.makingCopy(id2,how=>addSnapshot(root,`${id2.toLowerCase()}-before`,c.baseCommit,{...how,signal:ready})),placeStoryFiles(this.bp,c.repo.path,dir);let setup=c.repo.commands.setup?.trim();if(setup){doing("Setting up a copy of main's code for the failed test");let s=await runCommand(dir,setup,BEFORE_SHOTS_MS,{...this.guardedWatch(id2,dir),signal:ready});if(!s.ok)return this.log("warn",`Could not set up a copy of main's code for the failed tests (${setup}): ${tail(s.output,10)}`,id2),null;placeStoryFiles(this.bp,c.repo.path,dir)}return this.goOn(id2),await this.clearBeforeApp(id2),doing("Running the failed test on main's code"),await use(dir,{...this.guardedWatch(id2,dir),signal:within2(ON_MAIN_MS)})}catch(err2){if(stop?.aborted)throw err2;return this.log("warn",`Could not run the failed tests on main's code: ${err2.message}`,id2),null}finally{dir&&(await runsEnded([dir],6e3),await this.clearCopies([dir],{},id2).catch(()=>{}),await dropSnapshot(root,dir).catch(()=>{})),said!==void 0&&this.update(id2,x=>x.status=said)}}async screens(id2,when2,beside,again2=[]){let meta=this.meta(),cannot=!beside&&runsAsProduct(meta)?productPlan(this.bp,meta,readTicket(this.bp,id2)).why:void 0;if(cannot)throw new StepError(cannot,cannot);await this.readyBrowser(id2,beside?.signal??this.stops.get(id2)?.signal,beside?.job??id2);let t=this.update(id2,x=>{beside||(x.sessions={...x.sessions,tester:x.sessions?.tester??randomUUID7()},x.status=when2==="before"?"Tester is taking before screenshots":again2.length?"Tester is taking the screenshots again":"Tester is taking after screenshots",startStep(x,{who:"tester",step:"proof"})),when2==="after"&&!beside&&!again2.length&&x.proof?.length&&(x.proof=x.proof.filter(p=>p.beforeLabel!=="New screen").map(p=>({criterion:p.criterion,before:p.before,after:"",beforeLabel:p.beforeLabel,afterLabel:p.afterLabel}))),when2==="after"&&!beside&&!again2.length&&delete x.offScreen,addActivity(x,"tester",when2==="before"?"Taking screenshots before the change":"Taking screenshots after the change")});fs124.mkdirSync(this.bp.proofDir,{recursive:!0});let product=!beside&&runsAsProduct(meta),appCopy=this.appOf(t),rc=(appCopy?.repo??repoFor(meta,t)).commands,cwd=beside?.cwd??appCopy?.dir??t.worktree,signal2=beside?.signal??this.stops.get(id2)?.signal,start=rc.start?.trim()??"",url=rc.url?.trim()??"",shares=!beside&&!product,kept=shares?await this.keptFor(id2,cwd):null,tree=kept?.tree,app;kept?(app=kept.app,this.log("info",`squad-kit uses the app that already runs at ${app.url} for the Tester`,id2),this.update(id2,x=>addActivity(x,"tester","The app squad-kit started earlier still runs, so the Tester uses it"))):(shares&&(tree=await treeOf(cwd)),app=await this.startForPictures(id2,cwd,start,url,signal2,product,!!beside),this.log("info",`squad-kit started the app at ${app.url}${app.url!==url?` (${url} was used by another program)`:""}`,id2),this.update(id2,x=>addActivity(x,"tester","squad-kit started the app")));let at3=app.url,win=new AgentWindow({...this.windowBase(id2,"tester",cwd,beside?.job??id2),step:"proof",who:"Tester",tools:beside?ALL_TOOLS.filter(x=>x!=="run_checks"):[...ALL_TOOLS,"ask_api"],...beside?{}:{api:()=>this.apiParts(t)},app:{cwd,start,url,...this.guardFor(cwd,id2)?{guard:this.guardFor(cwd,id2)}:{},...product?this.productApp(id2):{}},...beside?{}:{checks:this.windowChecks(id2,cwd)},running:app,...shares?{keep:a=>this.keepApp(id2,cwd,a,tree)}:{},prefix:`${id2.toLowerCase()}-${when2}`,...signal2?{signal:signal2}:{}});openWindow(win);try{return await this.shoot(id2,t,when2,meta,win,at3,beside,again2)}finally{await closeWindow(win)}}async startForPictures(id2,cwd,start,url,signal2,product,beside){let onWait=holder=>{beside||this.update(id2,x=>x.status=holder==="checks"?"Waiting for the tests in this story's copy, then starting the app":"Waiting for the app starting in this story's copy")};try{return await inTurn2(cwd,"app",async()=>(beside||await this.clearBeforeApp(id2),beside||this.update(id2,x=>x.status="Starting the app"),product?this.startProductFor(id2):startPastOther(()=>startApp(cwd,start,url,void 0,signal2,this.guardFor(cwd,id2),{onHealed:detail=>this.healed(id2,detail)}),async pid=>!!folderOf(cwd,projectCopies(this.bp))&&(await this.clearCopies([cwd],{only:[pid]},id2)).length>0,text3=>this.log("info",text3,id2))),{...signal2?{signal:signal2}:{},onWait})}catch(err2){throw signal2?.aborted?err2:err2 instanceof TurnTimeout?new StepError(APP_WAITED,`The app ${err2.message}.`):err2 instanceof ProductError?new StepError(err2.message,err2.detail??err2.message):new StepError(dockerBlockedWords(err2.message,this.guardFor(cwd,id2))??NO_APP,err2.message)}}async shoot(id2,t,when2,meta,win,appUrl,beside,again2=[]){let lanes=beside?[await watchLane(beside.cwd,this.protectedFiles(beside.cwd),!0)]:await this.watchCopies(t,!0),since=Date.now(),lists=when2==="after"&&!beside?checklistsFor(this.bp,"tester"):[],prompt=`${proofPrompt(meta,t,when2,this.ctx(t),appUrl,when2==="after"?this.beforeSizes(t):void 0,beside?[]:Object.keys(this.apiParts(t)))}${checklistAsk(lists)}`,count3=doneWhenCount(t),complete2=answer2=>{let a=answer2??{},named2=new Set([...a.shots??[],...a.offScreen??[]].map(x=>Number(x?.criterion))),left=Array.from({length:count3},(_,i)=>i+1).filter(n=>!named2.has(n));return left.length?`every "done when" must be in shots or in offScreen, and ${left.map(n=>`"done when" ${n}`).join(", ")} ${left.length===1?"is":"are"} in neither`:null},realFiles=answer2=>{let wrong=[...new Set((answer2??{}).shots?.map(x=>path116.basename(String(x?.file??"")))??[])].filter(f=>!win.pictures.has(f));return wrong.length?`it names ${wrong.join(", ")}, which squad-kit did not take in this step`:null},notTooMany=answer2=>{let named2=(answer2??{}).shots?.map(x=>Number(x?.criterion))??[],over=[...new Set(named2)].filter(n=>named2.filter(m=>m===n).length>6);return over.length?`one "done when" takes at most ${6} pictures, one per part or case, and ${over.map(n=>`"done when" ${n}`).join(", ")} ${over.length===1?"has":"have"} more`:null},check=answer2=>realFiles(answer2)??notTooMany(answer2)??(when2==="after"&&!again2.length&&count3?complete2(answer2):null),out18=(await this.runAgent({role:"tester",step:"proof",ticket:id2,...beside?{job:beside.job,cwd:beside.cwd,sessionId:randomUUID7()}:{cwd:workFolder(t),sessionId:t.sessions.tester},prompt:again2.length?`${prompt}
1758
+ `),options:wayOptions({gate:"proof"})},x.waiting={kind:"stuck",summary:"The screenshots are not checked yet: needs one decision"}})}async notWorking(id2,rows2,notShown){let round=this.update(id2,x=>{x.proofRound=(x.proofRound??0)+1,x.fails=(x.fails??0)+1,x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"}}).proofRound;if(round>=2){let stop=brokenStop(rows2);this.update(id2,x=>{x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof-fix",x.status="The screenshots show it is not working yet",addActivity(x,"tester",`Still not working after ${round} rounds: ${rows2.map(r=>`"done when" ${r.criterion}`).join(", ")}`),x.stuck={gate:"proof",kind:"broken",question:stop.question,points:[...stop.points,...notShown.map(r=>`"Done when" ${r.criterion}, ${r.text}: ${r.why}`)],options:wayOptions({gate:"proof",kind:"broken"})},x.waiting={kind:"stuck",summary:"The screenshots show it is not working yet: needs one decision"}});return}this.goOn(id2),this.update(id2,x=>{addActivity(x,"tester",`The screenshots show ${rows2.length===1?'one "done when"':`${rows2.length} "done when"`} not working yet, so the Builder fixes ${rows2.length===1?"it":"them"}: ${rows2.map(r=>`"done when" ${r.criterion}: ${r.why}`).join(" ")}`),x.stage="implement",x.live=!0,x.status="Builder is fixing what the screenshots show"}),await this.runBuilder(id2,proofFixPrompt(rows2.map(fixLine),!readTicket(this.bp,id2)?.noNewTests),!0),this.afterProofFix(id2)}cannotShow(id2,rows2,retook=!0,noes=[]){let stop=rows2.length?notShownStop(rows2):{question:"The Tester still answers no on your team's checklist.",points:[]},listWords2=noes.map(n=>`Your team's checklist "${n.list}": ${n.question}${n.why?` (${n.why})`:""}`);this.update(id2,x=>{x.live=!1,x.squad=null,delete x.clock,x.stage="review",x.retryPhase="proof",x.gates={...x.gates??{checks:"pass",review:"pass"},proof:"fail"};let listOnly=!rows2.length;x.status=listOnly?"A no on your team's checklist":"The screenshots do not show it yet",addActivity(x,"tester",listOnly?`The Tester still answers no on your team's checklist after ${2} more tries`:retook?`Could not show ${rows2.map(r=>`"done when" ${r.criterion}`).join(", ")} on a screen after ${2} more tries`:'Every "done when" ended as not on a screen, so the screenshots do not show the work'),x.stuck={gate:"proof",kind:"not-shown",question:stop.question,points:[...stop.points,...listWords2],options:wayOptions({gate:"proof",kind:"not-shown"})},x.waiting={kind:"stuck",summary:`${x.status}: needs one decision`}})}async proofFix(id2){let t=await this.workspace(id2),rows2=brokenRows(t);if(!rows2.length)return this.proof(id2);this.update(id2,x=>{x.stage="implement",x.live=!0,x.status="Builder is fixing what the screenshots show"}),await this.runBuilder(id2,proofFixPrompt(rows2.map(fixLine),!t.noNewTests),!!t.sessions?.build),this.afterProofFix(id2)}afterProofFix(id2){this.update(id2,x=>{addActivity(x,"builder","Fixed what the screenshots showed"),x.reviewRound=0,x.stage="test",x.status="Running the project checks",scheduleSquad(x,"test",.2,1)})}shootBefore(id2){let key=`__before:${id2}`;if(this.running.has(key))return;let ended=()=>{};this.beforeShotsOf.set(id2,new Promise(resolve2=>ended=resolve2)),this.launch(key,()=>this.beforeShots(id2,key).finally(()=>{this.beforeShotsOf.delete(id2),ended()}))}async beforeShots(id2,key){let own2=this.stops.get(key),cutOff=!1,timer=setTimeout(()=>{cutOff=!0,own2.abort()},BEFORE_SHOTS_MS),t=readTicket(this.bp,id2),repo=appCopyOf(this.meta(),t)?.repo??repoFor(this.meta(),t),root=path116.join(projectRoot(this.bp),repo.path),dir=null;try{dir=await addSnapshot(root,`${id2.toLowerCase()}-before`,t.baseCommit,{signal:own2.signal});let setup=repo.commands.setup?.trim();if(setup){let r=await runCommand(dir,setup,BEFORE_SHOTS_MS,{signal:own2.signal,onHealed:detail=>this.healed(id2,detail),...this.guardFor(dir,id2)?{guard:this.guardFor(dir,id2)}:{}});if(own2.signal.aborted)throw new Error(STOPPED10);if(!r.ok)throw new Error(`setting up a copy of the code as it was failed (${setup}): ${tail(r.output,3)}`)}let left=(await this.screens(id2,"before",{cwd:dir,job:key,signal:own2.signal})).problems;left.length&&this.update(id2,x=>addActivity(x,"tester",`Left out before screenshots that did not pass squad-kit's check: ${left.map(b=>b.text).join(" ")}`))}catch(err2){if(cutOff||!own2.signal.aborted){let e=explainError(err2);cutOff||this.log("warn",`Could not take the before screenshots: ${[e.what,e.detail].filter(Boolean).join(": ")}`,id2),this.update(id2,x=>addActivity(x,"tester",cutOff?"Stopped the before screenshots after 8 minutes, so the proof shows the screens after the change only":"Could not take the before screenshots, so the proof shows the screens after the change only"))}}finally{clearTimeout(timer),dir&&(await runsEnded([dir],6e3),await this.clearCopies([dir],{},id2).catch(()=>{}),await dropSnapshot(root,dir))}}async testsOnMain(id2,c,r){if(!c.baseCommit)return null;let tests=testsToTry(r.output,c.dir,r.command,await workSoFar(c.dir,c.baseCommit));if(!tests)return null;let t=readTicket(this.bp,id2);if(tests.some(x=>t?.oldTests?.includes(testKey(x))))return null;let seen=t?.onMain?.find(m=>m.repo===c.repo.path&&m.base===c.baseCommit),found2={old:tests.filter(x=>seen?.old.includes(testKey(x))),fine:tests.filter(x=>seen?.fine.includes(testKey(x))),printed:{}},left=tests.filter(x=>!found2.old.includes(x)&&!found2.fine.includes(x));if(!left.length)return found2;let fresh=await this.inMainCopy(id2,c,(dir,watch)=>runOnMain(dir,r.command,left,(one2,ms2)=>runCommand(dir,one2,ms2,{silentMs:CHECK_SILENT_MS,...watch}),Date.now()+ON_MAIN_MS));return fresh?(found2.old.push(...fresh.old),found2.fine.push(...fresh.fine),Object.assign(found2.printed,fresh.printed),this.log("info",`Ran the failed tests on main's code (${c.baseCommit.slice(0,8)}): ${[...fresh.old.map(x=>`failed there too: ${testKey(x)}`),...fresh.fine.map(x=>`passed there: ${testKey(x)}`)].join("; ")}`,id2),this.update(id2,x=>{let rest=(x.onMain??[]).filter(m=>m.repo!==c.repo.path),keys=list2=>[...new Set(list2.map(testKey))];x.onMain=[...rest,{repo:c.repo.path,base:c.baseCommit,old:keys(found2.old),fine:keys(found2.fine)}]}),found2):null}async inMainCopy(id2,c,use){await this.beforeShotsOf.get(id2),this.goOn(id2);let root=path116.join(projectRoot(this.bp),c.repo.path),stop=this.stops.get(id2)?.signal,within2=ms2=>stop?AbortSignal.any([stop,AbortSignal.timeout(ms2)]):AbortSignal.timeout(ms2),said=readTicket(this.bp,id2)?.status,doing=text3=>this.update(id2,x=>{x.status=text3,stepDoing(x,text3)}),dir=null;try{let ready=within2(BEFORE_SHOTS_MS);dir=await this.makingCopy(id2,how=>addSnapshot(root,`${id2.toLowerCase()}-before`,c.baseCommit,{...how,signal:ready})),placeStoryFiles(this.bp,c.repo.path,dir);let setup=c.repo.commands.setup?.trim();if(setup){doing("Setting up a copy of main's code for the failed test");let s=await runCommand(dir,setup,BEFORE_SHOTS_MS,{...this.guardedWatch(id2,dir),signal:ready});if(!s.ok)return this.log("warn",`Could not set up a copy of main's code for the failed tests (${setup}): ${tail(s.output,10)}`,id2),null;placeStoryFiles(this.bp,c.repo.path,dir)}return this.goOn(id2),await this.clearBeforeApp(id2),doing("Running the failed test on main's code"),await use(dir,{...this.guardedWatch(id2,dir),signal:within2(ON_MAIN_MS)})}catch(err2){if(stop?.aborted)throw err2;return this.log("warn",`Could not run the failed tests on main's code: ${err2.message}`,id2),null}finally{dir&&(await runsEnded([dir],6e3),await this.clearCopies([dir],{},id2).catch(()=>{}),await dropSnapshot(root,dir).catch(()=>{})),said!==void 0&&this.update(id2,x=>x.status=said)}}async screens(id2,when2,beside,again2=[]){let meta=this.meta(),cannot=!beside&&runsAsProduct(meta)?productPlan(this.bp,meta,readTicket(this.bp,id2)).why:void 0;if(cannot)throw new StepError(cannot,cannot);await this.readyBrowser(id2,beside?.signal??this.stops.get(id2)?.signal,beside?.job??id2);let t=this.update(id2,x=>{beside||(x.sessions={...x.sessions,tester:x.sessions?.tester??randomUUID7()},x.status=when2==="before"?"Tester is taking before screenshots":again2.length?"Tester is taking the screenshots again":"Tester is taking after screenshots",startStep(x,{who:"tester",step:"proof"})),when2==="after"&&!beside&&!again2.length&&x.proof?.length&&(x.proof=x.proof.filter(p=>p.beforeLabel!=="New screen").map(p=>({criterion:p.criterion,before:p.before,after:"",beforeLabel:p.beforeLabel,afterLabel:p.afterLabel}))),when2==="after"&&!beside&&!again2.length&&delete x.offScreen,addActivity(x,"tester",when2==="before"?"Taking screenshots before the change":"Taking screenshots after the change")});fs124.mkdirSync(this.bp.proofDir,{recursive:!0});let product=!beside&&runsAsProduct(meta),appCopy=this.appOf(t),rc=(appCopy?.repo??repoFor(meta,t)).commands,cwd=beside?.cwd??appCopy?.dir??t.worktree,signal2=beside?.signal??this.stops.get(id2)?.signal,start=rc.start?.trim()??"",url=rc.url?.trim()??"",shares=!beside&&!product,setUp=shares?await this.setUpAgain(id2,appCopy??{repo:repoFor(meta,t),dir:cwd}):{ran:!1};setUp.failed&&this.log("warn",`Setting up the copy again did not work, so the app may not work (${setUp.failed.command}):
1759
+ ${tail(setUp.failed.output,30)}`,id2),setUp.ran&&await this.dropKept(id2);let kept=shares?await this.keptFor(id2,cwd):null,tree=kept?.tree,app;kept?(app=kept.app,this.log("info",`squad-kit uses the app that already runs at ${app.url} for the Tester`,id2),this.update(id2,x=>addActivity(x,"tester","The app squad-kit started earlier still runs, so the Tester uses it"))):(shares&&(tree=await treeOf(cwd)),app=await this.startForPictures(id2,cwd,start,url,signal2,product,!!beside),this.log("info",`squad-kit started the app at ${app.url}${app.url!==url?` (${url} was used by another program)`:""}`,id2),this.update(id2,x=>addActivity(x,"tester","squad-kit started the app")));let at3=app.url,win=new AgentWindow({...this.windowBase(id2,"tester",cwd,beside?.job??id2),step:"proof",who:"Tester",tools:beside?ALL_TOOLS.filter(x=>x!=="run_checks"):[...ALL_TOOLS,"ask_api"],...beside?{}:{api:()=>this.apiParts(t)},app:{cwd,start,url,...this.guardFor(cwd,id2)?{guard:this.guardFor(cwd,id2)}:{},...product?this.productApp(id2):{}},...beside?{}:{checks:this.windowChecks(id2,cwd)},running:app,...shares?{keep:a=>this.keepApp(id2,cwd,a,tree)}:{},prefix:`${id2.toLowerCase()}-${when2}`,...signal2?{signal:signal2}:{}});openWindow(win);try{return await this.shoot(id2,t,when2,meta,win,at3,beside,again2)}finally{await closeWindow(win)}}async startForPictures(id2,cwd,start,url,signal2,product,beside){let onWait=holder=>{beside||this.update(id2,x=>x.status=holder==="checks"?"Waiting for the tests in this story's copy, then starting the app":"Waiting for the app starting in this story's copy")};try{return await inTurn2(cwd,"app",async()=>(beside||await this.clearBeforeApp(id2),beside||this.update(id2,x=>x.status="Starting the app"),product?this.startProductFor(id2):startPastOther(()=>startApp(cwd,start,url,void 0,signal2,this.guardFor(cwd,id2),{onHealed:detail=>this.healed(id2,detail)}),async pid=>!!folderOf(cwd,projectCopies(this.bp))&&(await this.clearCopies([cwd],{only:[pid]},id2)).length>0,text3=>this.log("info",text3,id2))),{...signal2?{signal:signal2}:{},onWait})}catch(err2){throw signal2?.aborted?err2:err2 instanceof TurnTimeout?new StepError(APP_WAITED,`The app ${err2.message}.`):err2 instanceof ProductError?new StepError(err2.message,err2.detail??err2.message):new StepError(dockerBlockedWords(err2.message,this.guardFor(cwd,id2))??NO_APP,err2.message)}}async shoot(id2,t,when2,meta,win,appUrl,beside,again2=[]){let lanes=beside?[await watchLane(beside.cwd,this.protectedFiles(beside.cwd),!0)]:await this.watchCopies(t,!0),since=Date.now(),lists=when2==="after"&&!beside?checklistsFor(this.bp,"tester"):[],prompt=`${proofPrompt(meta,t,when2,this.ctx(t),appUrl,when2==="after"?this.beforeSizes(t):void 0,beside?[]:Object.keys(this.apiParts(t)))}${checklistAsk(lists)}`,count3=doneWhenCount(t),complete2=answer2=>{let a=answer2??{},named2=new Set([...a.shots??[],...a.offScreen??[]].map(x=>Number(x?.criterion))),left=Array.from({length:count3},(_,i)=>i+1).filter(n=>!named2.has(n));return left.length?`every "done when" must be in shots or in offScreen, and ${left.map(n=>`"done when" ${n}`).join(", ")} ${left.length===1?"is":"are"} in neither`:null},realFiles=answer2=>{let wrong=[...new Set((answer2??{}).shots?.map(x=>path116.basename(String(x?.file??"")))??[])].filter(f=>!win.pictures.has(f));return wrong.length?`it names ${wrong.join(", ")}, which squad-kit did not take in this step`:null},notTooMany=answer2=>{let named2=(answer2??{}).shots?.map(x=>Number(x?.criterion))??[],over=[...new Set(named2)].filter(n=>named2.filter(m=>m===n).length>6);return over.length?`one "done when" takes at most ${6} pictures, one per part or case, and ${over.map(n=>`"done when" ${n}`).join(", ")} ${over.length===1?"has":"have"} more`:null},check=answer2=>realFiles(answer2)??notTooMany(answer2)??(when2==="after"&&!again2.length&&count3?complete2(answer2):null),out18=(await this.runAgent({role:"tester",step:"proof",ticket:id2,...beside?{job:beside.job,cwd:beside.cwd,sessionId:randomUUID7()}:{cwd:workFolder(t),sessionId:t.sessions.tester},prompt:again2.length?`${prompt}
1759
1760
 
1760
1761
  ${proofAgainPrompt(again2)}`:prompt,permissions:["read"],schema:PROOF_SCHEMA,check,checkHelp:()=>win.pictureList(),timeoutMs:PROOF_STEP_MS,addDirs:this.extraDirs(t),box:{},win,...beside?{}:{onTool:this.watcher(id2,"tester",workFolder(t))}}).finally(()=>this.keepLanes(id2,"tester",lanes,[this.bp.proofDir]))).structured??{shots:[]},say=text3=>this.update(id2,x=>addActivity(x,"tester",text3));lists.length&&this.update(id2,x=>this.listNoes.set(id2,answerLists(this.bp,x,lists,out18.checklist)));let shots=[];for(let s of out18.shots??[]){let file3=path116.basename(String(s.file??"")),taken=win.pictures.get(file3);if(!taken){say(`The Tester named ${file3||"a picture"} for "done when" ${s.criterion}, which squad-kit did not take in this step, so it is left out`);continue}if(shots.filter(x=>x.criterion===s.criterion).length>=6){say(`The Tester named more than ${6} pictures for "done when" ${s.criterion}, so squad-kit kept the first ${6}`);continue}let note2=s.note?this.plainNote(win,s.note):"";shots.push({criterion:s.criterion,file:file3,page:taken.page,size:taken.size,shows:verdictOf(s.shows)??"not-shown",...note2?{note:note2}:{}})}let off=[];for(let o of when2==="after"?out18.offScreen??[]:[]){let n=Number(o?.criterion);if(!criterionText2(t,n)||off.some(x=>x.criterion===n))continue;let refused=win.refusals.length,why=this.plainNote(win,String(o.why??""));win.refusals.length>refused||off.push({criterion:n,why})}let skipped=[...off.map(o=>o.why),this.plainNote(win,out18.skipped??"")].filter(Boolean).join(" ");if(win.tooMany)throw new StepError(tooManyWords("Tester"),win.refusalDetail());if(win.tooManyMistakes)throw new StepError(tooManyMistakesWords("Tester"),win.mistakeDetail());let named=n=>criterionText2(t,n),problems=[],rejected=new Set;for(let s of shots){let full=path116.join(this.bp.proofDir,s.file),criterion=named(s.criterion);if(!criterion||!fs124.existsSync(full))continue;let bad2=isApiProof(s.file)?null:shotProblem(full,since);bad2&&(rejected.add(s.file),problems.push({criterion,text:`The ${when2} screenshot for "${criterion}" (${s.file}): ${bad2}.`}))}return this.update(id2,x=>{let proof=x.proof??[],usable=shots.filter(s=>!rejected.has(s.file)&&named(s.criterion)&&fs124.existsSync(path116.join(this.bp.proofDir,s.file)));for(let n of new Set(usable.map(s=>s.criterion))){let criterion=named(n),own2=usable.filter(s=>s.criterion===n),row3=proof.find(p=>p.criterion===criterion);row3||(row3={criterion,before:"",after:"",beforeLabel:"Before",afterLabel:"After"},proof.push(row3)),when2==="before"?row3.before=own2[0].file:(addPictures(row3,own2,(a,b)=>this.sameFile(a,b)),this.pairWithBefore(row3))}if(x.proof=proof.map(p=>p.before?p:{...p,before:p.after,beforeLabel:"New screen",afterLabel:"After"}),when2==="after"){let kept=again2.length?x.offScreen??[]:[],added2=off.map(o=>({criterion:named(o.criterion),why:o.why||"The Tester says it does not show on a screen."})),all=[...kept.filter(k=>!added2.some(a=>a.criterion===k.criterion)),...added2];all.length?x.offScreen=all:delete x.offScreen;let notOn=off.length?`. Not on a screen: ${off.map(o=>`"done when" ${o.criterion}`).join(", ")}`:skipped?`. Skipped: ${skipped}`:"",answers2=shots.filter(s=>isApiProof(s.file)).length,pictures=shots.length-answers2-rejected.size;addActivity(x,"tester",`Saved ${pictures} ${answers2&&pictures===1?"screenshot":"screenshots"}${answers2?` and ${answers2} ${answers2===1?"answer":"answers"} from the API`:""}${notOn}`)}}),{problems,planned:shots.map(s=>s.criterion),said:skipped,stuck:win.stuck,sizes:Object.fromEntries(shots.map(s=>[s.file,s.size]))}}async readyBrowser(id2,signal2,job){let beat2=setInterval(()=>this.dog.beat(job),2e4);try{await prepareShots({...signal2?{signal:signal2}:{},onLine:text3=>this.update(id2,x=>addActivity(x,"tester",text3))})}catch(err2){throw signal2?.aborted?err2:new StepError(err2.message,shotsErrorDetail(err2))}finally{clearInterval(beat2)}}plainNote(win,text3){let said=text3.trim();if(!said)return"";let verdict=judgeQuestion({text:said});if(!verdict.pass&&verdict.kind==="not-allowed")return win.refuse("note",`${verdict.why}: "${said.slice(0,200)}"`,refusedAskLine("Tester",verdict.what)),"";let{kept,left}=plainParts(said,technicalWords);return left&&this.log("info",`Left out of the Tester's note: ${left.slice(0,600)}`,win.ticket),kept?kept.slice(0,600):(this.update(win.ticket,x=>addActivity(x,"tester","The Tester wrote something technical, so it stays out of the proof.")),"")}},NO_APP="squad-kit could not start the app, so it took no screenshots. Press Try again to try once more.",NO_PICTURES="squad-kit could not take the screenshots this time. Press Try again to try once more.",STUCK_PICTURES="The Tester got stuck on something technical that squad-kit could not fix, so there are no screenshots yet. Press Try again to try once more.";function technicalWords(sentence){let v=judgeQuestion({text:sentence});return!v.pass&&v.kind==="technical"}function checkNames(names2){let words2={test:"the tests",lint:"the lint check",build:"the build",start:"starting the app"},w=[...new Set(names2.map(n=>words2[n]??`the ${n} check`))];return w.length>1?`${w.slice(0,-1).join(", ")} and ${w.at(-1)}`:w[0]??"a check"}function missingShots(t,round){let count3=doneWhenCount(t),off=offScreenNumbers(t),out18=[];for(let n=1;n<=count3;n+=1)(t.proof??[]).find(p=>p.criterion===criterionText2(t,n))?.after||off.has(n)||out18.push({criterion:n,why:round.planned.includes(n)?"squad-kit could not use the picture the Tester took.":"The Tester took no picture of it."});return out18}var SAID={met:"it is met",broken:"it is broken","not-shown":"it is not shown","off-screen":"it is not on a screen"};function openStep(t){let i=(t.steps??[]).findIndex(s=>s.status!=="done");return i>=0?i+1:1}function held2(status){return!!(status&&(status.startsWith(QUEUED2)||isLimitText(status)||waitsForFirst(status)))}function isInside(dir,file3){return typeof file3=="string"&&!path116.relative(dir,file3).startsWith("..")}function clean3(c){return Object.fromEntries(Object.entries(c).filter(([,v])=>typeof v=="string"&&v.trim()))}function attachmentDirs(t){return[...new Set((t.attachments??[]).map(a=>path116.dirname(a)))]}var WaitingForYou=class extends Error{constructor(){super("Waiting for your answer")}};import{execSync as execSync2}from"child_process";import fs127 from"fs";import path119 from"path";import fs126 from"fs";import path118 from"path";import yaml17 from"js-yaml";import fs125 from"fs";import path117 from"path";function infoFile(bp){return bp.consoleFile}function readConsoleInfo(bp){try{return JSON.parse(fs125.readFileSync(infoFile(bp),"utf8"))}catch{return null}}function writeConsoleInfo(bp,info2){fs125.mkdirSync(bp.localDir,{recursive:!0}),fs125.writeFileSync(infoFile(bp),`${JSON.stringify(info2,null,2)}
1761
1762
  `,{encoding:"utf8",mode:384})}function appFile(){return path117.join(squadHome(),"app.json")}function readAppInfo(){try{return JSON.parse(fs125.readFileSync(appFile(),"utf8"))}catch{return null}}function writeAppInfo(info2){fs125.mkdirSync(squadHome(),{recursive:!0}),fs125.writeFileSync(appFile(),`${JSON.stringify(info2,null,2)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squad-kit",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "author": "Momen Yasser <momenysr@gmail.com>",
5
5
  "description": "Your AI Engineering Squad transforms ideas into production-ready software.",
6
6
  "keywords": [