squad-kit 1.3.6 → 1.3.7

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 +14 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -313,7 +313,7 @@ ${patterns.join(`
313
313
  `)}
314
314
  `}function parseStackDoc(text3){let[,stackText="",patternText=""]=text3.split(/^## (?:Stack|Patterns)$/m),facts=body10=>{let lines3=body10.split(`
315
315
  `),fact2=key=>lines3.find(l=>l.startsWith(`- ${key}: `))?.slice(key.length+4),decision=fact2("Decision"),where=fact2("Where"),role=TECH_ROLES.find(r=>r===fact2("Role")),rest=lines3.filter(l=>!/^- (Decision|Where|Role): /.test(l));return{decision:decision!==void 0?dec(decision):void 0,role,where:where?where.split(" | ").filter(Boolean).map(dec):[],text:block(rest.join(`
316
- `))}};return{stack:sections(stackText,/^### (.*)$/).map(s=>{let f=facts(s.text);return{name:dec(s.match[1]),why:f.text,...f.role?{role:f.role}:{},...f.decision!==void 0?{decision:f.decision}:{}}}),patterns:sections(patternText,/^### (.*)$/).map(s=>{let f=facts(s.text);return{name:dec(s.match[1]),what:f.text,where:f.where,...f.decision!==void 0?{decision:f.decision}:{}}})}}var LAYOUT=2;function hash(s){return createHash("sha1").update(s).digest("hex").slice(0,8)}function projectIdOf(squadDir){try{let raw=yaml6.load(fs51.readFileSync(path43.join(squadDir,"config.yaml"),"utf8"));if(raw?.board?.id)return String(raw.board.id)}catch{}let root=path43.resolve(path43.dirname(squadDir));return`${path43.basename(root).replace(/[^\w.-]+/g,"-")||"project"}-${hash(root)}`}function boardPaths(squadDir){let squad=path43.resolve(squadDir),projectId=projectIdOf(squad),localDir=path43.join(squadHome(),"projects",projectId);return{root:path43.dirname(squad),squadDir:squad,configFile:path43.join(squad,"config.yaml"),ticketsDir:path43.join(squad,"tickets"),decisionsDir:path43.join(squad,"decisions"),releasesDir:path43.join(squad,"releases"),rulesDir:path43.join(squad,"rules"),knowledgeDir:path43.join(squad,"knowledge"),wikiDir:path43.join(squad,"knowledge","wiki"),projectId,localDir,localMetaFile:path43.join(localDir,"board.yaml"),stateDir:path43.join(localDir,"state"),chatsDir:path43.join(localDir,"chats"),proofDir:path43.join(localDir,"proof"),mediaDir:path43.join(localDir,"files"),consoleFile:path43.join(localDir,"console.json"),pidFile:path43.join(localDir,"squad.pid"),secretsFile:path43.join(localDir,"secrets.yaml"),legacyDir:path43.join(squad,"board")}}function hasLegacyBoard(bp){return fs51.existsSync(path43.join(bp.legacyDir,"board.yaml"))&&!configBoard(bp)}function boardExists(bp){return!!configBoard(bp)}function now(){return new Date().toISOString()}function writeFile(file3,text3){if(fs51.existsSync(file3)&&fs51.readFileSync(file3,"utf8")===text3)return;fs51.mkdirSync(path43.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs51.writeFileSync(tmp,text3,"utf8"),fs51.renameSync(tmp,file3)}function dumpYaml(data){return yaml6.dump(JSON.parse(JSON.stringify(data)),{lineWidth:110,noRefs:!0})}function readYaml(file3){return fs51.existsSync(file3)?yaml6.load(fs51.readFileSync(file3,"utf8")):void 0}function readText(file3){return fs51.existsSync(file3)?fs51.readFileSync(file3,"utf8"):void 0}var cache2=new Map;function stampOf(files){return files.map(f=>fs51.existsSync(f)?`${fs51.statSync(f).mtimeMs}:${fs51.statSync(f).size}`:"-").join("|")}function cached2(key,files,read6){let stamp=stampOf(files),hit=cache2.get(key);if(hit&&hit.stamp===stamp)return structuredClone(hit.value);let value=read6();return cache2.set(key,{stamp,value}),structuredClone(value)}var PROJECT_META=["id","layout","documentsInGit","quality","allowedHosts","network","guard","project","key","nextNumber","engine","commands","noTests","alwaysNewTests","git","repos","parts","atOnce","delivery","localFiles","lockedFiles","database"],KNOWLEDGE_META=["map","lessons"],BOARD_FIELDS={id:"text",project:"text",key:"text",nextNumber:"number",engine:"text",quality:"text",delivery:"text",atOnce:"number",documentsInGit:"yes-no",alwaysNewTests:"yes-no",git:"object",commands:"object",guard:"object",repos:"objects",parts:"objects",localFiles:"objects",lockedFiles:"objects",database:"object",allowedHosts:"texts",network:"text"};function configBoard(bp){try{let board6=yaml6.load(fs51.readFileSync(bp.configFile,"utf8"))?.board;return isPlain(board6)&&!wrongField(board6,BOARD_FIELDS)?board6:void 0}catch{return}}function settingsProblem(bp){if(!fs51.existsSync(bp.configFile))return null;try{let raw=yaml6.load(fs51.readFileSync(bp.configFile,"utf8"));if(raw&&typeof raw=="object"&&(raw.board===void 0||configBoard(bp)))return null}catch{}return"This project's settings file could not be read, so its board cannot open. Nothing is lost. An engineer can look at it."}function knowledgeFiles(bp){return{project:path43.join(bp.knowledgeDir,"project.md"),lessons:path43.join(bp.knowledgeDir,"lessons.md"),stack:path43.join(bp.knowledgeDir,"stack.md")}}function readMeta(bp){let k=knowledgeFiles(bp);return cached2(`meta:${bp.squadDir}`,[bp.configFile,bp.localMetaFile,k.project,k.lessons,k.stack],()=>{let board6=configBoard(bp);if(!board6)throw new Error(`No board in ${bp.squadDir}. Open it with: squad console`);let local=readYaml(bp.localMetaFile)??{},project=readText(k.project),known=project?parseProjectDoc(project):{map:[]},lessons=readText(k.lessons),stack=readText(k.stack),meta={...local,...board6,map:known.map,lessons:lessons?parseLessonsDoc(lessons):[]};return(local.scan||known.summary!==void 0)&&(meta.scan={status:"done",...local.scan,...known.summary!==void 0?{summary:known.summary}:{}}),(local.blueprint||stack)&&(meta.blueprint={status:"done",...local.blueprint,...stack?parseStackDoc(stack):{stack:[],patterns:[]}}),meta})}function writeMeta(bp,meta){let full={...meta,id:meta.id??bp.projectId,layout:meta.layout??LAYOUT},project=Object.fromEntries(PROJECT_META.filter(key=>full[key]!==void 0).map(key=>[key,full[key]])),local={...full};for(let key of[...PROJECT_META,...KNOWLEDGE_META])delete local[key];let{summary,...scanLocal}=full.scan??{},{stack,patterns,...blueprintLocal}=full.blueprint??{};full.scan&&(local.scan=scanLocal),full.blueprint&&(local.blueprint=blueprintLocal);let config2=fs51.existsSync(bp.configFile)?loadConfig(bp.configFile):{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:full.project}};JSON.stringify(config2.board)!==JSON.stringify(JSON.parse(JSON.stringify(project)))&&(fs51.mkdirSync(bp.squadDir,{recursive:!0}),saveConfig(bp.configFile,{...config2,board:JSON.parse(JSON.stringify(project))}));let k=knowledgeFiles(bp);(full.map?.length||summary!==void 0||fs51.existsSync(k.project))&&writeFile(k.project,projectDoc(summary,full.map??[])),(full.lessons?.length||fs51.existsSync(k.lessons))&&writeFile(k.lessons,lessonsDoc(full.lessons??[])),(stack?.length||patterns?.length||fs51.existsSync(k.stack))&&writeFile(k.stack,stackDoc({stack:stack??[],patterns:patterns??[]})),writeFile(bp.localMetaFile,dumpYaml(local))}var TICKET_HOME={id:"project",title:"project",ask:"project",kind:"project",source:"project",priority:"project",stage:"project",track:"project",noNewTests:"project",createdAt:"project",createdBy:"project",owner:"project",handedOver:"project",archived:"project",builder:"project",reviewer:"project",repo:"project",branch:"project",follows:"project",work:"project",gates:"project",specApproved:"project",overrides:"project",blips:"local",outdated:"project",release:"project",foundIn:"project",severity:"project",pr:"project",mergedAt:"project",setsUp:"project",brief:"project",story:"project",criteria:"project",releaseNote:"project",alsoChanged:"project",added:"project",decision:"project",notes:"project",proof:"project",offScreen:"project",mockup:"project",changes:"project",changeOf:"project",dependsOn:"project",dependency:"project",breakdown:"project",decided:"project",questions:"project",steps:"project",attachments:"project",status:"local",live:"local",progress:"local",waiting:"local",activity:"local",squad:"local",sessions:"local",worktree:"local",baseCommit:"local",hookOwed:"project",hookFixTried:"local",tries:"local",fails:"local",reviewRound:"local",proofRound:"local",retryPhase:"local",capRaise:"local",createKey:"local",stopRequested:"local",reviewNotes:"local",localSetup:"local",setupTrouble:"local",setupOwed:"local",parts:"project",storyDir:"local",copies:"local",replanFor:"local",interrupted:"local",selfAccept:"local",quiet:"local",quietSnoozedUntil:"local",trackerQueue:"local",trackerRetry:"local",checksContinued:"local",oldTests:"project",waitsToStart:"local",onMain:"local",weakenedContinued:"local",addedSkipped:"local",testSuggestion:"local",testChange:"local",testDecided:"local",reviewAsked:"local",builtPlan:"local",notesSeen:"local",takenOver:"local",outside:"project",stuck:"local",clashedPr:"local",prClosed:"local",changeRequest:"local",agentQuestion:"local",chat:"local",planReview:"local",usage:"local",history:"local",quality:"local",budgetOk:"local",clock:"local",stageTimes:"local",retryOnStop:"local",deleting:"local",baseWanted:"local",seenOn:"local",ownerDeleted:"local"};function ticketDir(bp,id2){return path43.join(bp.ticketsDir,id2.toUpperCase())}function ticketFilesDir(bp,id2){return path43.join(ticketDir(bp,id2),"files")}function ticketMediaDir(bp,id2){return path43.join(bp.mediaDir,id2.toUpperCase())}var DOC_FILES=["ticket.md","spec.md","questions.md","design.md","notes.md","proof.md"];function stateFile(bp,id2){return path43.join(bp.stateDir,`${id2.toUpperCase()}.yaml`)}function ticketStateFile(bp,id2){return stateFile(bp,id2)}function inside2(dir,file3){let rel=path43.relative(dir,file3);return!!rel&&!rel.startsWith("..")&&!path43.isAbsolute(rel)}function splitTicket(bp,t){let folder=ticketDir(bp,t.id),project={},state={};for(let[key,home2]of Object.entries(TICKET_HOME))t[key]!==void 0&&(home2==="project"?project[key]=t[key]:state[key]=t[key]);t.steps&&(state.stepState=t.steps.map(({title:_title,...rest})=>rest));let talk2=Object.fromEntries((t.questions??[]).filter(q4=>q4.thread!==void 0||q4.thinking!==void 0).map(q4=>[q4.id,{...q4.thread!==void 0?{thread:q4.thread}:{},...q4.thinking!==void 0?{thinking:q4.thinking}:{}}]));Object.keys(talk2).length&&(state.questionState=talk2);let files=(t.attachments??[]).filter(f=>inside2(folder,f)).map(f=>path43.relative(folder,f)),localFiles=(t.attachments??[]).filter(f=>!inside2(folder,f));localFiles.length&&(state.localFiles=localFiles),delete project.attachments,t.mockup&&(project.mockup=inside2(folder,t.mockup)?path43.relative(folder,t.mockup):t.mockup);let input7={...project,...files.length?{files}:{}};return{docs:ticketDocs(input7),state}}function joinTicket(bp,id2,docs,state){let folder=ticketDir(bp,id2),p=readTicketDocs(docs),{stepState,questionState,localFiles,...live}=state,t={...p,...live};p.steps&&(t.steps=p.steps.map((s,i)=>({...s,...stepState?.[i]??{}}))),p.questions&&(t.questions=p.questions.map(q4=>({...q4,...questionState?.[q4.id]??{}})));let files=[...(p.files??[]).map(f=>path43.join(folder,f)),...localFiles??[]];return delete t.files,files.length&&(t.attachments=files),t.mockup&&!path43.isAbsolute(t.mockup)&&(t.mockup=path43.join(folder,t.mockup)),t.activity??=[],t.waiting??=null,t.status??="",t.stuck?.kind==="not-met"&&(t.stuck.kind="broken"),oldStop(t)&&clearStaleStop(t),t.takenOver?.free&&showOutsideStep(t),dropOldChecked(t),t}var UnreadableTicket=class extends Error{constructor(id2,detail,file3="ticket.md"){super(`${id2} could not be read: its file is damaged, so it is not on the board. Nothing is lost. An engineer can look at it.`);this.id=id2;this.detail=detail;this.file=file3;this.name="UnreadableTicket"}id;detail;file},STAGES=new Set([...BOARD_STAGES,"done"]),TICKET_FIELDS={kind:"text",priority:"text",track:"text",owner:"text",builder:"text",reviewer:"text",repo:"text",branch:"text",release:"text",foundIn:"text",severity:"text",mergedAt:"text",changeOf:"text",mockup:"text",parts:"texts",follows:"texts",dependsOn:"texts",alsoChanged:"texts",oldTests:"texts",decided:"objects",added:"objects",overrides:"objects",outdated:"objects",work:"object",gates:"object",brief:"object",pr:"object",specApproved:"object",dependency:"object",breakdown:"object",handedOver:"object",outside:"object",archived:"object",setsUp:"yes-no",hookOwed:"yes-no",noNewTests:"yes-no"};function shapeProblem(t,id2){for(let key of["id","title","createdAt","createdBy"])if(typeof t[key]!="string"||!t[key])return`It has no ${key}.`;return t.id.toUpperCase()!==id2.toUpperCase()?`Its id says ${t.id}, in the folder of ${id2.toUpperCase()}.`:STAGES.has(String(t.stage))?typeof t.source?.type!="string"?"It does not say where it came from.":wrongField(t,TICKET_FIELDS):`"${String(t.stage)}" is not a stage.`}function docProblem(docs,t){for(let[file3,text3]of Object.entries(docs))if(hasMergeMarks(text3))return{file:file3,detail:"It still has the marks of a merge in it."};let says={"spec.md":()=>t.story!==void 0||t.criteria!==void 0||t.releaseNote!==void 0||t.changes!==void 0,"questions.md":()=>!!t.questions?.length,"design.md":()=>!!(t.decision||t.steps),"notes.md":()=>!!t.notes?.length,"proof.md":()=>!!t.proof?.length};for(let[file3,read6]of Object.entries(says))if(docs[file3]?.replace(/^# .*$/m,"").trim()&&!read6())return{file:file3,detail:"It holds none of the parts squad-kit reads from that file."};return null}var TicketFileProblem=class extends Error{constructor(file3,detail){super(detail);this.file=file3}file};function readTicketFiles(bp,id2){let folder=ticketDir(bp,id2),files=[...DOC_FILES.map(f=>path43.join(folder,f)),stateFile(bp,id2)];if(!fs51.existsSync(files[0]))return null;try{return cached2(`ticket:${folder}`,files,()=>{let docs=Object.fromEntries(DOC_FILES.map(f=>[f,readText(path43.join(folder,f))?.replace(/\r\n/g,`
316
+ `))}};return{stack:sections(stackText,/^### (.*)$/).map(s=>{let f=facts(s.text);return{name:dec(s.match[1]),why:f.text,...f.role?{role:f.role}:{},...f.decision!==void 0?{decision:f.decision}:{}}}),patterns:sections(patternText,/^### (.*)$/).map(s=>{let f=facts(s.text);return{name:dec(s.match[1]),what:f.text,where:f.where,...f.decision!==void 0?{decision:f.decision}:{}}})}}var LAYOUT=2;function hash(s){return createHash("sha1").update(s).digest("hex").slice(0,8)}function projectIdOf(squadDir){try{let raw=yaml6.load(fs51.readFileSync(path43.join(squadDir,"config.yaml"),"utf8"));if(raw?.board?.id)return String(raw.board.id)}catch{}let root=path43.resolve(path43.dirname(squadDir));return`${path43.basename(root).replace(/[^\w.-]+/g,"-")||"project"}-${hash(root)}`}function boardPaths(squadDir){let squad=path43.resolve(squadDir),projectId=projectIdOf(squad),localDir=path43.join(squadHome(),"projects",projectId);return{root:path43.dirname(squad),squadDir:squad,configFile:path43.join(squad,"config.yaml"),ticketsDir:path43.join(squad,"tickets"),decisionsDir:path43.join(squad,"decisions"),releasesDir:path43.join(squad,"releases"),rulesDir:path43.join(squad,"rules"),knowledgeDir:path43.join(squad,"knowledge"),wikiDir:path43.join(squad,"knowledge","wiki"),projectId,localDir,localMetaFile:path43.join(localDir,"board.yaml"),stateDir:path43.join(localDir,"state"),chatsDir:path43.join(localDir,"chats"),proofDir:path43.join(localDir,"proof"),mediaDir:path43.join(localDir,"files"),consoleFile:path43.join(localDir,"console.json"),pidFile:path43.join(localDir,"squad.pid"),secretsFile:path43.join(localDir,"secrets.yaml"),legacyDir:path43.join(squad,"board")}}function hasLegacyBoard(bp){return fs51.existsSync(path43.join(bp.legacyDir,"board.yaml"))&&!configBoard(bp)}function boardExists(bp){return!!configBoard(bp)}function now(){return new Date().toISOString()}function writeFile(file3,text3){if(fs51.existsSync(file3)&&fs51.readFileSync(file3,"utf8")===text3)return;fs51.mkdirSync(path43.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs51.writeFileSync(tmp,text3,"utf8"),fs51.renameSync(tmp,file3)}function dumpYaml(data){return yaml6.dump(JSON.parse(JSON.stringify(data)),{lineWidth:110,noRefs:!0})}function readYaml(file3){return fs51.existsSync(file3)?yaml6.load(fs51.readFileSync(file3,"utf8")):void 0}function readText(file3){return fs51.existsSync(file3)?fs51.readFileSync(file3,"utf8"):void 0}var cache2=new Map;function stampOf(files){return files.map(f=>fs51.existsSync(f)?`${fs51.statSync(f).mtimeMs}:${fs51.statSync(f).size}`:"-").join("|")}function cached2(key,files,read6){let stamp=stampOf(files),hit=cache2.get(key);if(hit&&hit.stamp===stamp)return structuredClone(hit.value);let value=read6();return cache2.set(key,{stamp,value}),structuredClone(value)}var PROJECT_META=["id","layout","documentsInGit","quality","allowedHosts","network","guard","project","key","nextNumber","engine","commands","noTests","alwaysNewTests","git","repos","parts","atOnce","delivery","localFiles","lockedFiles","database"],KNOWLEDGE_META=["map","lessons"],BOARD_FIELDS={id:"text",project:"text",key:"text",nextNumber:"number",engine:"text",quality:"text",delivery:"text",atOnce:"number",documentsInGit:"yes-no",alwaysNewTests:"yes-no",git:"object",commands:"object",guard:"object",repos:"objects",parts:"objects",localFiles:"objects",lockedFiles:"objects",database:"object",allowedHosts:"texts",network:"text"};function configBoard(bp){try{let board6=yaml6.load(fs51.readFileSync(bp.configFile,"utf8"))?.board;return isPlain(board6)&&!wrongField(board6,BOARD_FIELDS)?board6:void 0}catch{return}}function settingsProblem(bp){if(!fs51.existsSync(bp.configFile))return null;try{let raw=yaml6.load(fs51.readFileSync(bp.configFile,"utf8"));if(raw&&typeof raw=="object"&&(raw.board===void 0||configBoard(bp)))return null}catch{}return"This project's settings file could not be read, so its board cannot open. Nothing is lost. An engineer can look at it."}function knowledgeFiles(bp){return{project:path43.join(bp.knowledgeDir,"project.md"),lessons:path43.join(bp.knowledgeDir,"lessons.md"),stack:path43.join(bp.knowledgeDir,"stack.md")}}function readMeta(bp){let k=knowledgeFiles(bp);return cached2(`meta:${bp.squadDir}`,[bp.configFile,bp.localMetaFile,k.project,k.lessons,k.stack],()=>{let board6=configBoard(bp);if(!board6)throw new Error(`No board in ${bp.squadDir}. Open it with: squad console`);let local=readYaml(bp.localMetaFile)??{},project=readText(k.project),known=project?parseProjectDoc(project):{map:[]},lessons=readText(k.lessons),stack=readText(k.stack),meta={...local,...board6,map:known.map,lessons:lessons?parseLessonsDoc(lessons):[]};return(local.scan||known.summary!==void 0)&&(meta.scan={status:"done",...local.scan,...known.summary!==void 0?{summary:known.summary}:{}}),(local.blueprint||stack)&&(meta.blueprint={status:"done",...local.blueprint,...stack?parseStackDoc(stack):{stack:[],patterns:[]}}),meta})}function writeMeta(bp,meta){let full={...meta,id:meta.id??bp.projectId,layout:meta.layout??LAYOUT},project=Object.fromEntries(PROJECT_META.filter(key=>full[key]!==void 0).map(key=>[key,full[key]])),local={...full};for(let key of[...PROJECT_META,...KNOWLEDGE_META])delete local[key];let{summary,...scanLocal}=full.scan??{},{stack,patterns,...blueprintLocal}=full.blueprint??{};full.scan&&(local.scan=scanLocal),full.blueprint&&(local.blueprint=blueprintLocal);let config2=fs51.existsSync(bp.configFile)?loadConfig(bp.configFile):{...DEFAULT_CONFIG,project:{...DEFAULT_CONFIG.project,name:full.project}};JSON.stringify(config2.board)!==JSON.stringify(JSON.parse(JSON.stringify(project)))&&(fs51.mkdirSync(bp.squadDir,{recursive:!0}),saveConfig(bp.configFile,{...config2,board:JSON.parse(JSON.stringify(project))}));let k=knowledgeFiles(bp);(full.map?.length||summary!==void 0||fs51.existsSync(k.project))&&writeFile(k.project,projectDoc(summary,full.map??[])),(full.lessons?.length||fs51.existsSync(k.lessons))&&writeFile(k.lessons,lessonsDoc(full.lessons??[])),(stack?.length||patterns?.length||fs51.existsSync(k.stack))&&writeFile(k.stack,stackDoc({stack:stack??[],patterns:patterns??[]})),writeFile(bp.localMetaFile,dumpYaml(local))}var TICKET_HOME={id:"project",title:"project",ask:"project",kind:"project",source:"project",priority:"project",stage:"project",track:"project",noNewTests:"project",createdAt:"project",createdBy:"project",owner:"project",handedOver:"project",archived:"project",builder:"project",reviewer:"project",repo:"project",branch:"project",follows:"project",work:"project",gates:"project",specApproved:"project",overrides:"project",blips:"local",outdated:"project",release:"project",foundIn:"project",severity:"project",pr:"project",mergedAt:"project",setsUp:"project",brief:"project",story:"project",criteria:"project",releaseNote:"project",alsoChanged:"project",added:"project",decision:"project",notes:"project",proof:"project",offScreen:"project",mockup:"project",changes:"project",changeOf:"project",dependsOn:"project",dependency:"project",breakdown:"project",decided:"project",questions:"project",steps:"project",attachments:"project",status:"local",live:"local",progress:"local",waiting:"local",activity:"local",squad:"local",sessions:"local",worktree:"local",baseCommit:"local",hookOwed:"project",hookFixTried:"local",tries:"local",fails:"local",reviewRound:"local",proofRound:"local",retryPhase:"local",capRaise:"local",createKey:"local",stopRequested:"local",reviewNotes:"local",localSetup:"local",setupTrouble:"local",setupFor:"local",setupOwed:"local",parts:"project",storyDir:"local",copies:"local",replanFor:"local",interrupted:"local",selfAccept:"local",quiet:"local",quietSnoozedUntil:"local",trackerQueue:"local",trackerRetry:"local",checksContinued:"local",oldTests:"project",waitsToStart:"local",onMain:"local",weakenedContinued:"local",addedSkipped:"local",testSuggestion:"local",testChange:"local",testDecided:"local",reviewAsked:"local",builtPlan:"local",notesSeen:"local",takenOver:"local",outside:"project",stuck:"local",clashedPr:"local",prClosed:"local",changeRequest:"local",agentQuestion:"local",chat:"local",planReview:"local",usage:"local",history:"local",quality:"local",budgetOk:"local",clock:"local",stageTimes:"local",retryOnStop:"local",deleting:"local",baseWanted:"local",seenOn:"local",ownerDeleted:"local"};function ticketDir(bp,id2){return path43.join(bp.ticketsDir,id2.toUpperCase())}function ticketFilesDir(bp,id2){return path43.join(ticketDir(bp,id2),"files")}function ticketMediaDir(bp,id2){return path43.join(bp.mediaDir,id2.toUpperCase())}var DOC_FILES=["ticket.md","spec.md","questions.md","design.md","notes.md","proof.md"];function stateFile(bp,id2){return path43.join(bp.stateDir,`${id2.toUpperCase()}.yaml`)}function ticketStateFile(bp,id2){return stateFile(bp,id2)}function inside2(dir,file3){let rel=path43.relative(dir,file3);return!!rel&&!rel.startsWith("..")&&!path43.isAbsolute(rel)}function splitTicket(bp,t){let folder=ticketDir(bp,t.id),project={},state={};for(let[key,home2]of Object.entries(TICKET_HOME))t[key]!==void 0&&(home2==="project"?project[key]=t[key]:state[key]=t[key]);t.steps&&(state.stepState=t.steps.map(({title:_title,...rest})=>rest));let talk2=Object.fromEntries((t.questions??[]).filter(q4=>q4.thread!==void 0||q4.thinking!==void 0).map(q4=>[q4.id,{...q4.thread!==void 0?{thread:q4.thread}:{},...q4.thinking!==void 0?{thinking:q4.thinking}:{}}]));Object.keys(talk2).length&&(state.questionState=talk2);let files=(t.attachments??[]).filter(f=>inside2(folder,f)).map(f=>path43.relative(folder,f)),localFiles=(t.attachments??[]).filter(f=>!inside2(folder,f));localFiles.length&&(state.localFiles=localFiles),delete project.attachments,t.mockup&&(project.mockup=inside2(folder,t.mockup)?path43.relative(folder,t.mockup):t.mockup);let input7={...project,...files.length?{files}:{}};return{docs:ticketDocs(input7),state}}function joinTicket(bp,id2,docs,state){let folder=ticketDir(bp,id2),p=readTicketDocs(docs),{stepState,questionState,localFiles,...live}=state,t={...p,...live};p.steps&&(t.steps=p.steps.map((s,i)=>({...s,...stepState?.[i]??{}}))),p.questions&&(t.questions=p.questions.map(q4=>({...q4,...questionState?.[q4.id]??{}})));let files=[...(p.files??[]).map(f=>path43.join(folder,f)),...localFiles??[]];return delete t.files,files.length&&(t.attachments=files),t.mockup&&!path43.isAbsolute(t.mockup)&&(t.mockup=path43.join(folder,t.mockup)),t.activity??=[],t.waiting??=null,t.status??="",t.stuck?.kind==="not-met"&&(t.stuck.kind="broken"),oldStop(t)&&clearStaleStop(t),t.takenOver?.free&&showOutsideStep(t),dropOldChecked(t),t}var UnreadableTicket=class extends Error{constructor(id2,detail,file3="ticket.md"){super(`${id2} could not be read: its file is damaged, so it is not on the board. Nothing is lost. An engineer can look at it.`);this.id=id2;this.detail=detail;this.file=file3;this.name="UnreadableTicket"}id;detail;file},STAGES=new Set([...BOARD_STAGES,"done"]),TICKET_FIELDS={kind:"text",priority:"text",track:"text",owner:"text",builder:"text",reviewer:"text",repo:"text",branch:"text",release:"text",foundIn:"text",severity:"text",mergedAt:"text",changeOf:"text",mockup:"text",parts:"texts",follows:"texts",dependsOn:"texts",alsoChanged:"texts",oldTests:"texts",decided:"objects",added:"objects",overrides:"objects",outdated:"objects",work:"object",gates:"object",brief:"object",pr:"object",specApproved:"object",dependency:"object",breakdown:"object",handedOver:"object",outside:"object",archived:"object",setsUp:"yes-no",hookOwed:"yes-no",noNewTests:"yes-no"};function shapeProblem(t,id2){for(let key of["id","title","createdAt","createdBy"])if(typeof t[key]!="string"||!t[key])return`It has no ${key}.`;return t.id.toUpperCase()!==id2.toUpperCase()?`Its id says ${t.id}, in the folder of ${id2.toUpperCase()}.`:STAGES.has(String(t.stage))?typeof t.source?.type!="string"?"It does not say where it came from.":wrongField(t,TICKET_FIELDS):`"${String(t.stage)}" is not a stage.`}function docProblem(docs,t){for(let[file3,text3]of Object.entries(docs))if(hasMergeMarks(text3))return{file:file3,detail:"It still has the marks of a merge in it."};let says={"spec.md":()=>t.story!==void 0||t.criteria!==void 0||t.releaseNote!==void 0||t.changes!==void 0,"questions.md":()=>!!t.questions?.length,"design.md":()=>!!(t.decision||t.steps),"notes.md":()=>!!t.notes?.length,"proof.md":()=>!!t.proof?.length};for(let[file3,read6]of Object.entries(says))if(docs[file3]?.replace(/^# .*$/m,"").trim()&&!read6())return{file:file3,detail:"It holds none of the parts squad-kit reads from that file."};return null}var TicketFileProblem=class extends Error{constructor(file3,detail){super(detail);this.file=file3}file};function readTicketFiles(bp,id2){let folder=ticketDir(bp,id2),files=[...DOC_FILES.map(f=>path43.join(folder,f)),stateFile(bp,id2)];if(!fs51.existsSync(files[0]))return null;try{return cached2(`ticket:${folder}`,files,()=>{let docs=Object.fromEntries(DOC_FILES.map(f=>[f,readText(path43.join(folder,f))?.replace(/\r\n/g,`
317
317
  `)]).filter(([,v])=>v!==void 0)),t=joinTicket(bp,id2.toUpperCase(),docs,readYaml(stateFile(bp,id2))??{}),wrong=shapeProblem(t,id2);if(wrong)throw new Error(wrong);let doc=docProblem(docs,t);if(doc)throw new TicketFileProblem(doc.file,doc.detail);return t})}catch(err2){throw new UnreadableTicket(id2.toUpperCase(),err2.message,err2 instanceof TicketFileProblem?err2.file:void 0)}}function idNumber(id2){return Number(id2.replace(/^\D+/,"").replace(/\D.*$/,""))||0}function listTickets(bp){return fs51.existsSync(bp.ticketsDir)?fs51.readdirSync(bp.ticketsDir,{withFileTypes:!0}).filter(d=>d.isDirectory()).map(d=>readableTicket(bp,d.name)).filter(t=>!!t).sort((a,b)=>idNumber(a.id)-idNumber(b.id)||a.id.localeCompare(b.id)):[]}function readTicket(bp,id2){return readTicketFiles(bp,id2)}function readableTicket(bp,id2){try{return readTicketFiles(bp,id2)}catch{return null}}function ticketProblem(bp,id2){try{return readTicketFiles(bp,id2),null}catch(err2){return err2.message}}function brokenTickets(bp,listed3=listTickets(bp)){if(!fs51.existsSync(bp.ticketsDir))return[];let read6=new Set(listed3.map(t=>t.id)),out18=[];for(let d of fs51.readdirSync(bp.ticketsDir,{withFileTypes:!0}))if(!(!d.isDirectory()||read6.has(d.name.toUpperCase())))try{readTicketFiles(bp,d.name)}catch(err2){let e=err2;out18.push({id:e.id,problem:e.message,file:path43.relative(bp.root,path43.join(ticketDir(bp,e.id),e.file)),detail:e.detail})}return out18.sort((a,b)=>idNumber(a.id)-idNumber(b.id)||a.id.localeCompare(b.id))}function requireTicket(bp,id2){let ticket=readTicket(bp,id2);if(!ticket)throw new Error(`No ticket ${id2.toUpperCase()} on this board.`);return ticket}function writeTicket(bp,ticket){let folder=ticketDir(bp,ticket.id),{docs,state}=splitTicket(bp,ticket);for(let f of DOC_FILES){let file3=path43.join(folder,f);docs[f]!==void 0?writeFile(file3,docs[f]):fs51.existsSync(file3)&&fs51.rmSync(file3)}writeFile(stateFile(bp,ticket.id),dumpYaml(state))}function updateTicket(bp,id2,change){let ticket=requireTicket(bp,id2),stage=ticket.stage;return change(ticket),trackStage(ticket,stage),waitsNow(ticket)&&pauseClock(ticket),writeTicket(bp,ticket),ticket}function removeTicket(bp,id2){fs51.rmSync(ticketDir(bp,id2),{recursive:!0,force:!0}),fs51.rmSync(stateFile(bp,id2),{force:!0}),fs51.rmSync(ticketMediaDir(bp,id2),{recursive:!0,force:!0})}function branchNumbers(bp){if(!fs51.existsSync(path43.join(bp.root,".git")))return new Set;let refs=git(bp.root,["for-each-ref","--format=%(refname)","refs/heads/squad","refs/remotes/*/squad/*"],{allowFail:!0});return new Set(refs.split(`
318
318
  `).map(r=>r.split("/squad/")[1]??"").filter(Boolean))}function nextTicketId(bp){let meta=readMeta(bp),id2=`${meta.key}-${meta.nextNumber}`,taken=branchNumbers(bp);for(;fs51.existsSync(ticketDir(bp,id2))||taken.has(id2.toLowerCase());)meta.nextNumber+=1,id2=`${meta.key}-${meta.nextNumber}`;return meta.nextNumber+=1,writeMeta(bp,meta),id2}function addActivity(ticket,who4,text3){ticket.activity.unshift({at:now(),who:who4,text:text3})}var RECORD_WORD={decision:"Decision",release:"Release",breakdown:"Breakdown"};function recordProblemWords(kind,id2){return`${RECORD_WORD[kind]} ${id2} could not be read: its file is damaged, so it is not shown here. Nothing is lost. An engineer can look at it.`}function readDoc(file3,kind){return cached2(`doc:${file3}`,[file3],()=>{let text3=fs51.readFileSync(file3,"utf8"),name=path43.basename(file3,".md"),detail,x=null;try{detail=hasMergeMarks(text3)?"It still has the marks of a merge in it.":null,x=kind.parse(text3),detail??=kind.problem(x,name)}catch(err2){detail=err2.message}return detail?{value:null,detail:kind.named.test(name)||kind.claims.test(text3)?detail:null}:{value:x,detail:null}})}function docFiles(dir){return fs51.existsSync(dir)?fs51.readdirSync(dir).filter(f=>f.endsWith(".md")).map(f=>path43.join(dir,f)):[]}function listDocs(dir,kind){return docFiles(dir).map(f=>readDoc(f,kind).value).filter(x=>x!==null)}function brokenDocs(bp,dir,kind){return docFiles(dir).flatMap(f=>{let{detail}=readDoc(f,kind),id2=path43.basename(f,".md");return detail?[{kind:kind.kind,id:id2,problem:recordProblemWords(kind.kind,id2),file:path43.relative(bp.root,f),detail}]:[]})}function dayText(d){return d instanceof Date?d.toISOString().slice(0,10):d}var DECISION_FIELDS={from:"text",approvedBy:"text",date:"text",area:"text",replaces:"text",replacedBy:"text",tickets:"texts",buildsOn:"texts",proposed:"yes-no"},DECISION_DOC={kind:"decision",parse(text3){let d=parseDecisionDoc(text3);return d.date=dayText(d.date),typeof d.id=="string"&&typeof d.title=="string"&&Object.assign(d,{tickets:d.tickets??[],from:d.from??"",approvedBy:d.approvedBy??"",date:d.date??"",checkedBy:d.checkedBy??null}),d},problem(d,name){return typeof d.id!="string"||!/^[\w.-]+$/.test(d.id)?"It has no id.":typeof d.title!="string"||!d.title?"It has no title.":DECISION_DOC.named.test(name)&&d.id!==name?`Its id says ${d.id}, in the file of ${name}.`:d.checkedBy!==null&&(typeof d.checkedBy!="object"||Array.isArray(d.checkedBy))?'"checkedBy" is not in the right form.':wrongField(d,DECISION_FIELDS)},named:/^([A-Za-z][\w-]*\.\d+|ADR-\d+)$/,claims:/^(id: |## Chosen$)/m};function listDecisions(bp){return listDocs(bp.decisionsDir,DECISION_DOC).sort((a,b)=>String(b.date).localeCompare(String(a.date))||idNumber(b.id)-idNumber(a.id)||b.id.localeCompare(a.id))}function brokenDecisions(bp){return brokenDocs(bp,bp.decisionsDir,DECISION_DOC)}function writeDecision(bp,decision){writeFile(path43.join(bp.decisionsDir,`${decision.id}.md`),decisionDoc(decision))}function nextDecisionId(bp,ticketId){let prefix=ticketId?ticketId.toUpperCase():"STACK",taken=new Set([...listDecisions(bp).map(d=>d.id),...brokenDecisions(bp).map(d=>d.id)]),n=1;for(;taken.has(`${prefix}.${n}`);)n+=1;return`${prefix}.${n}`}function versionKey(v){return v.replace(/^v/,"").split(".").map(Number)}function releaseStateFile(bp,version){return path43.join(bp.stateDir,"releases",`${version}.yaml`)}function withReleaseState(bp,r){let state;try{state=readYaml(releaseStateFile(bp,r.version))}catch{state=void 0}return{...r,...Array.isArray(state?.log)?{log:state.log}:{},...Array.isArray(state?.unpushed)&&state.unpushed.length?{unpushed:state.unpushed}:{}}}var RELEASE_FIELDS={from:"text",date:"text",whatsappDraft:"text",tickets:"texts",bugs:"texts",out:"texts",addons:"object",titles:"object",gateStop:"objects"},RELEASE_DOC={kind:"release",parse(text3){let r=parseReleaseDoc(text3);return r.date=dayText(r.date),r.date===void 0&&delete r.date,typeof r.version=="string"&&Object.assign(r,{tickets:r.tickets??[],from:r.from??"",addons:r.addons??{changelog:!0,tag:!0,pipeline:!0,whatsapp:!0}}),r},problem(r,name){return typeof r.version!="string"||!/^v\d+\.\d+\.\d+[\w.-]*$/.test(r.version)?"It has no version.":r.status!=="draft"&&r.status!=="released"?`"${String(r.status)}" is not a release's state.`:RELEASE_DOC.named.test(name)&&r.version!==name?`Its version says ${r.version}, in the file of ${name}.`:wrongField(r,RELEASE_FIELDS)},named:/^v\d+\.\d+\.\d+[\w.-]*$/,claims:/^(version: |## Notes$)/m};function listReleases(bp){return listDocs(bp.releasesDir,RELEASE_DOC).map(r=>withReleaseState(bp,r)).sort((a,b)=>{let[x,y]=[versionKey(a.version),versionKey(b.version)];for(let i=0;i<3;i+=1)if(x[i]!==y[i])return(y[i]??0)-(x[i]??0);return 0})}function brokenReleases(bp){return brokenDocs(bp,bp.releasesDir,RELEASE_DOC)}function readRelease(bp,version){let v=version.startsWith("v")?version:`v${version}`,file3=path43.join(bp.releasesDir,`${v}.md`);if(!fs51.existsSync(file3))return null;let{value,detail}=readDoc(file3,RELEASE_DOC);if(detail)throw new Error(recordProblemWords("release",v));return value?withReleaseState(bp,value):null}function writeRelease(bp,release){writeFile(path43.join(bp.releasesDir,`${release.version}.md`),releaseDoc(release)),release.log||release.unpushed?.length?writeFile(releaseStateFile(bp,release.version),dumpYaml({...release.log?{log:release.log}:{},...release.unpushed?.length?{unpushed:release.unpushed}:{}})):fs51.rmSync(releaseStateFile(bp,release.version),{force:!0})}function removeRelease(bp,version){fs51.rmSync(path43.join(bp.releasesDir,`${version}.md`),{force:!0}),fs51.rmSync(releaseStateFile(bp,version),{force:!0})}function draftRelease(bp){return listReleases(bp).find(r=>r.status==="draft")??null}function latestRelease(bp){return listReleases(bp).find(r=>r.status==="released")??null}function codeChanged(bp){let meta=readMeta(bp);if(meta.engine==="live")return meta.blueprint?.status==="pending"||meta.blueprint?.status==="running"||(meta.blueprint={stack:[],patterns:[],...meta.blueprint,status:"pending"},writeMeta(bp,meta)),meta.blueprint}function queueImpactCheck(bp,adr){let meta=readMeta(bp);meta.engine!=="live"||meta.impactChecks?.some(c=>c.adr===adr)||writeMeta(bp,{...meta,impactChecks:[...meta.impactChecks??[],{adr,status:"pending"}]})}function clearBoard(bp){for(let dir of[bp.ticketsDir,bp.decisionsDir,bp.releasesDir,bp.rulesDir,bp.knowledgeDir,bp.localDir])fs51.rmSync(dir,{recursive:!0,force:!0})}function newProjectId(name){return`${name.replace(/[^\w.-]+/g,"-").toLowerCase()||"project"}-${randomUUID3().slice(0,8)}`}var KNOWN_BAD_GROUPS=[{name:"Tunnels that open this computer to the internet",hosts:["ngrok.io","*.ngrok.io","ngrok.app","*.ngrok.app","ngrok-free.app","*.ngrok-free.app","ngrok.dev","*.ngrok.dev","ngrok-free.dev","*.ngrok-free.dev","*.ngrok.com","ngrok-agent.com","*.ngrok-agent.com","trycloudflare.com","*.trycloudflare.com","loca.lt","*.loca.lt","localtunnel.me","*.localtunnel.me","serveo.net","*.serveo.net","serveousercontent.com","*.serveousercontent.com","pagekite.me","*.pagekite.me","pagekite.net","*.pagekite.net","localhost.run","*.localhost.run","lhr.life","*.lhr.life","pinggy.io","*.pinggy.io","pinggy.link","*.pinggy.link","bore.pub","*.bore.pub","tunnelmole.net","*.tunnelmole.net","localxpose.io","*.localxpose.io","loclx.io","*.loclx.io","zrok.io","*.zrok.io","*.devtunnels.ms","sharedwithexpose.com","*.sharedwithexpose.com","localtonet.com","*.localtonet.com","localto.net","*.localto.net","playit.gg","*.playit.gg"]},{name:"Paste and file-drop sites",hosts:["pastebin.com","*.pastebin.com","paste.ee","hastebin.com","termbin.com","ix.io","sprunge.us","paste.rs","dpaste.com","dpaste.org","rentry.co","rentry.org","controlc.com","justpaste.it","privatebin.net","pastes.io","paste.c-net.org","transfer.sh","*.transfer.sh","file.io","0x0.st","temp.sh","bashupload.com","oshi.at","keep.sh","filebin.net","tmpfiles.org","catbox.moe","*.catbox.moe","gofile.io","*.gofile.io","uguu.se","*.uguu.se","pixeldrain.com"]},{name:"Request catchers and out-of-band test hosts",hosts:["webhook.site","*.webhook.site","requestbin.net","*.requestbin.net","*.m.pipedream.net","requestcatcher.com","*.requestcatcher.com","hookbin.com","postb.in","ptsv2.com","ptsv3.com","requestrepo.com","*.requestrepo.com","interact.sh","*.interact.sh","interactsh.com","*.interactsh.com","*.oast.pro","*.oast.live","*.oast.site","*.oast.online","*.oast.fun","*.oast.me","burpcollaborator.net","*.burpcollaborator.net","oastify.com","*.oastify.com","canarytokens.com","*.canarytokens.com","canarytokens.org","*.canarytokens.org","dnslog.cn","*.dnslog.cn","ceye.io","*.ceye.io","*.xss.ht"]}],RECOMMENDED_GROUPS=[{name:"npm",hosts:["registry.npmjs.org","registry.npmjs.com","npmjs.org","npmjs.com","skimdb.npmjs.com","npm.pkg.github.com","registry.yarnpkg.com","repo.yarnpkg.com","yarnpkg.com","get.pnpm.io","registry.bower.io"]},{name:"Node.js, Bun and Deno",hosts:["nodejs.org","deb.nodesource.com","bun.sh","deno.land","dl.deno.land","jsr.io"]},{name:"Prisma",hosts:["binaries.prisma.sh"]},{name:"PyPI",hosts:["pypi.org","pypi.python.org","files.pythonhosted.org","*.pythonhosted.org","pip.pypa.io","bootstrap.pypa.io","install.python-poetry.org","conda.anaconda.org","repo.anaconda.com","anaconda.org","conda.binstar.org","binstar.org","repo.continuum.io","download.pytorch.org"]},{name:"RubyGems",hosts:["rubygems.org","index.rubygems.org","api.rubygems.org","bundler.rubygems.org","rubygems.pkg.github.com","gems.rubyonrails.org","cache.ruby-lang.org","*.rvm.io"]},{name:"Maven and Gradle",hosts:["repo.maven.apache.org","repo1.maven.org","maven.apache.org","search.maven.org","maven-central.storage-download.googleapis.com","maven.pkg.github.com","oss.sonatype.org","repo.spring.io","maven.oracle.com","repo.grails.org","download.eclipse.org","services.gradle.org","plugins.gradle.org","plugins-artifacts.gradle.org","downloads.gradle.org","downloads.gradle-dn.com","gradle.org"]},{name:"Java and Kotlin downloads",hosts:["jdk.java.net","api.adoptium.net","adoptium.net","download.oracle.com","www.java.com","cache-redirector.jetbrains.com","download.jetbrains.com","packages.jetbrains.team","maven.pkg.jetbrains.space"]},{name:"Google Maven and Android",hosts:["maven.google.com","dl.google.com","dl-ssl.google.com"]},{name:"NuGet and .NET",hosts:["api.nuget.org","globalcdn.nuget.org","nuget.org","www.nuget.org","dist.nuget.org","nuget.pkg.github.com","pkgs.dev.azure.com","nugetregistryv2prod.blob.core.windows.net","azuresearch-usnc.nuget.org","azuresearch-ussc.nuget.org","dot.net","dotnet.microsoft.com","builds.dotnet.microsoft.com","ci.dot.net","dotnetcli.blob.core.windows.net","dotnetcli.azureedge.net","dotnetbuilds.azureedge.net","download.visualstudio.microsoft.com","aka.ms"]},{name:"Dart and Flutter",hosts:["pub.dev","pub.dartlang.org","storage.googleapis.com","storage.flutter-io.cn"]},{name:"Packagist",hosts:["repo.packagist.org","packagist.org","getcomposer.org"]},{name:"Go modules",hosts:["proxy.golang.org","sum.golang.org","go.dev","golang.org","pkg.go.dev","goproxy.io"]},{name:"crates.io",hosts:["crates.io","index.crates.io","static.crates.io","static.rust-lang.org","sh.rustup.rs"]},{name:"CocoaPods and Swift",hosts:["cocoapods.org","cdn.cocoapods.org","trunk.cocoapods.org","swift.org","download.swift.org"]},{name:"Haskell and Perl",hosts:["haskell.org","*.hackage.haskell.org","hackage.haskell.org","get-ghcup.haskell.org","downloads.haskell.org","cpan.org","www.cpan.org","metacpan.org","cpan.metacpan.org"]},{name:"Elixir, R and Julia",hosts:["hex.pm","repo.hex.pm","builds.hex.pm","cdn.hex.pm","cran.r-project.org","cloud.r-project.org","pkg.julialang.org","julialang-s3.julialang.org"]},{name:"Bazel",hosts:["bcr.bazel.build","releases.bazel.build","mirror.bazel.build"]},{name:"GitHub downloads",hosts:["github.com","codeload.github.com","objects.githubusercontent.com","release-assets.githubusercontent.com","raw.githubusercontent.com","*.githubusercontent.com","lfs.github.com","github-cloud.s3.amazonaws.com","ghcr.io"]},{name:"Browsers for browser tests",hosts:["cdn.playwright.dev","playwright.download.prss.microsoft.com","playwright.azureedge.net","playwright-akamai.azureedge.net","playwright-verizon.azureedge.net","download.cypress.io","cdn.cypress.io","googlechromelabs.github.io","edgedl.me.gvt1.com"]},{name:"Container images",hosts:["registry.hub.docker.com","*.docker.io","*.docker.com","quay.io","mcr.microsoft.com","gcr.io","public.ecr.aws"]},{name:"Linux packages",hosts:["archive.ubuntu.com","security.ubuntu.com","ppa.launchpad.net","keyserver.ubuntu.com","deb.debian.org","security.debian.org","apt.llvm.org","dl.fedoraproject.org","mirrors.fedoraproject.org","download.fedoraproject.org","dl-cdn.alpinelinux.org","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","formulae.brew.sh"]},{name:"Terraform and Kubernetes",hosts:["releases.hashicorp.com","registry.terraform.io","dl.k8s.io","pkgs.k8s.io"]},{name:"JSON schemas",hosts:["json-schema.org","json.schemastore.org"]},{name:"Certificate checks",hosts:["ocsp.digicert.com","crl3.digicert.com","crl4.digicert.com","ocsp.sectigo.com","crl.sectigo.com","ocsp.usertrust.com","crl.usertrust.com","ocsp.globalsign.com","crl.globalsign.com","ocsp.identrust.com","crl.identrust.com","ocsp.ssl.com","crls.ssl.com","oneocsp.microsoft.com","*.o.lencr.org","*.c.lencr.org"]}],KNOWN_BAD=[...new Set(KNOWN_BAD_GROUPS.flatMap(g=>g.hosts))],RECOMMENDED=[...new Set(RECOMMENDED_GROUPS.flatMap(g=>g.hosts))];function hostMatches(host,pattern){let h=host.toLowerCase().replace(/\.$/,""),p=pattern.toLowerCase();return p==="*"?!0:p.startsWith("*.")?h.endsWith(p.slice(1)):h===p}function knownBadGroup(host){let bare=host.toLowerCase().replace(/^\*\./,"");return KNOWN_BAD_GROUPS.find(g=>g.hosts.some(p=>hostMatches(bare,p)))?.name??null}var PACKAGE_HOSTS=RECOMMENDED,DEFAULT_NETWORK="standard",NETWORK_LEVELS=["standard","strict","off"],NETWORK_NAME={standard:"Standard",strict:"Strict",off:"Off"},NETWORK_WORDS={standard:"Any website but known-bad ones such as tunnels, paste sites and request catchers, with your keys hidden so a harmful package has nothing to take.",strict:"Only the official package and download sites of common tools and the websites you allow, while your project's own tests and app still reach any site for now.",off:"No internet, only this computer, so the setup, the tests and the app must work with what is already here."};function parseNetwork(s){let v=s?.trim().toLowerCase();return NETWORK_LEVELS.find(l=>l===v)??null}function projectNetwork(root){try{return parseNetwork(readMeta(boardPaths(path44.join(root,".squad"))).network)??DEFAULT_NETWORK}catch{return DEFAULT_NETWORK}}function networkPolicy(level=DEFAULT_NETWORK,sites=[],platform=process.platform){let own2=sites.filter(h=>!knownBadGroup(h)),hosts=level==="off"?[]:[...new Set([...level==="standard"?["*"]:[],...PACKAGE_HOSTS,...own2])];return{level,hosts,noHosts:KNOWN_BAD,commands:level==="off"&&platform==="darwin"?"this-computer":"internet"}}var MAX_BLOCKED_SITES=50;function blockedSitesFile(bp){return path44.join(bp.localDir,"blocked-sites.json")}function blockedSites(bp){try{let list2=JSON.parse(fs52.readFileSync(blockedSitesFile(bp),"utf8"));return Array.isArray(list2)?list2.filter(b=>b&&typeof b.host=="string").reverse():[]}catch{return[]}}function noteBlockedSite(bp,site){try{let list2=blockedSites(bp).reverse().filter(b=>b.host!==site.host||b.story!==site.story);list2.push({host:site.host,story:site.story,level:site.level,at:site.at??new Date().toISOString()}),fs52.mkdirSync(bp.localDir,{recursive:!0}),fs52.writeFileSync(blockedSitesFile(bp),`${JSON.stringify(list2.slice(-MAX_BLOCKED_SITES),null,1)}
319
319
  `)}catch{}}var PACKAGE_CACHES=[".npm/_cacache",".yarn/berry/cache","Library/Caches/Yarn",".cache/yarn","Library/pnpm/store",".local/share/pnpm/store","Library/Caches/pnpm",".cache/pnpm",".cargo/registry",".cargo/git","go/pkg/mod"],PRIVATE=[".ssh",".aws",".gnupg",".config/gh",".azure",".kube",".docker/config.json",".netrc",".git-credentials"],PUSH_ADDRESSES=["/",".","file://","https://","http://","ssh://","git://","git@"];function noPushEnv(env2=process.env){let from=Number(env2.GIT_CONFIG_COUNT)||0,out18={GIT_CONFIG_COUNT:String(from+PUSH_ADDRESSES.length)};return PUSH_ADDRESSES.forEach((address,i)=>{out18[`GIT_CONFIG_KEY_${from+i}`]="url.squad-kit-no-push://.pushInsteadOf",out18[`GIT_CONFIG_VALUE_${from+i}`]=address}),out18}function ticketCacheDir(bp,id2){return path44.join(bp.localDir,"caches",id2.toLowerCase())}function projectCacheDir(bp){return path44.join(bp.localDir,"caches","project")}function dropTicketCache(bp,id2){fs52.rmSync(ticketCacheDir(bp,id2),{recursive:!0,force:!0})}function packageCacheEnv(dir,env2=process.env){let at3=(...p)=>path44.join(dir,...p),gems=[env2.GEM_HOME,...(env2.GEM_PATH??"").split(path44.delimiter)].filter(p=>!!p?.trim());return{NUGET_PACKAGES:at3("nuget","packages"),NUGET_HTTP_CACHE_PATH:at3("nuget","http-cache"),NUGET_PLUGINS_CACHE_PATH:at3("nuget","plugins-cache"),MAVEN_OPTS:[env2.MAVEN_OPTS?.trim(),`-Dmaven.repo.local=${at3("maven","repository")}`].filter(Boolean).join(" "),GRADLE_USER_HOME:at3("gradle"),GEM_HOME:at3("ruby","gems"),GEM_PATH:`${[...new Set(gems)].join(path44.delimiter)}${path44.delimiter}`,GEM_SPEC_CACHE:at3("ruby","specs"),BUNDLE_PATH:at3("ruby","bundle"),BUNDLE_USER_CACHE:at3("ruby","bundle-cache")}}function gitPlaces(cwd){let link=path44.join(cwd,".git");try{let own2=path44.resolve(cwd,/^gitdir: (.*)$/m.exec(fs52.readFileSync(link,"utf8"))[1]);return[link,own2,path44.resolve(own2,fs52.readFileSync(path44.join(own2,"commondir"),"utf8").trim())]}catch{return[link]}}function withReal(dirs){let out18=new Set;for(let d of dirs){out18.add(d);try{out18.add(fs52.realpathSync(d))}catch{}}return[...out18]}function sandboxAsk(o){let home2=o.home??os7.homedir(),temp=withReal([os7.tmpdir(),...process.platform==="win32"?[]:["/tmp"]]),caches=PACKAGE_CACHES.map(c=>path44.join(home2,c)).filter(c=>fs52.existsSync(c)),env2=[".env",".env.*","**/.env","**/.env.*"].map(p=>`${o.root}/${p}`),own2=o.cache?withReal([o.cache]):[],net4=networkPolicy(o.network??projectNetwork(o.root),o.hosts??[]);return{write:[...new Set([...withReal([o.proof]),...own2,...temp,...caches])],noRead:[...new Set([...PRIVATE.map(p=>path44.join(home2,p)),...withReal([squadHome()]),...o.secrets,...env2])],read:[...new Set(withReal([o.cwd,o.proof,...o.cache?[o.cache]:[],...o.read]))],noWrite:[...new Set(withReal([...gitPlaces(o.cwd),...copyDirsIn(o.cwd).flatMap(gitPlaces)]))],hosts:net4.hosts,noHosts:net4.noHosts}}function sandboxOf(tool5){return tool5.sandbox?.()??{available:!1,why:`${tool5.label} has no sandbox squad-kit can use`}}function sandboxStatus(){return AI_TOOLS.filter(t=>t.canRun).map(t=>({tool:t.label,...sandboxOf(t)}))}function guardNote(platform=process.platform,tools4=sandboxStatus()){return platform!=="win32"||tools4.some(t=>t.available)?null:{title:"Windows: less protected",text:"On Windows, squad-kit does not guard the agents' own commands, or the project's own tests and app, so they can reach other files on this computer. Use it on projects you trust."}}function shown(file3,home2){return file3===home2?"~":file3.startsWith(`${home2}${path44.sep}`)?`~${file3.slice(home2.length)}`:file3}function blockedLine(b,home2=os7.homedir()){if(b.kind==="host")return`A download from ${b.target} was not allowed for this project; the work goes on without it.`;let file3=shown(b.target,home2);if(b.kind==="write")return`Writing ${file3} was blocked: the squad writes only in the ticket's copy, the proof folder and temp folders.`;let base2=path44.basename(b.target),why=/^\.env(\.|$)/.test(base2)?"the project's .env files stay private":b.target.startsWith(squadHome())?"squad-kit's own files stay private":"keys and sign-ins stay private";return`Reading ${file3} was blocked: ${why}.`}function blockedIn(output){if(!output)return[];for(let t of AI_TOOLS){let found2=t.blocked?.(output)??[];if(found2.length)return found2}return[]}function parseHosts(list2){let hosts=[],bad2=[],closed=[];for(let raw of list2.flatMap(x=>String(x).split(/[\s,]+/))){let s=raw.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//,"").replace(/[/?#].*$/,"").replace(/:\d+$/,"");raw.trim()&&(!/^(\*\.)?([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(s)||s==="*"?bad2.push(raw.trim()):knownBadGroup(s)?closed.push(s):hosts.push(s))}return{hosts:[...new Set(hosts)],bad:bad2,closed}}function hostsProblem(p){return p.bad.length?`"${p.bad[0]}" is not a host name. Write it like nexus.example.com, or *.example.com for all of its subdomains.`:p.closed.length?`${p.closed[0]} stays closed for every project: it is one of the ${knownBadGroup(p.closed[0]).toLowerCase()}, which a build never needs.`:null}import{createHash as createHash2}from"crypto";import fs53 from"fs";import os8 from"os";import path45 from"path";function storiesCacheDir(bp){return path45.join(bp.localDir,"caches","stories")}function ownFolder(dir){let at3=fs53.lstatSync(dir,{throwIfNoEntry:!1});at3&&!at3.isDirectory()&&fs53.rmSync(dir,{force:!0}),fs53.mkdirSync(dir,{recursive:!0})}function inside3(dir,p){try{let home2=fs53.realpathSync(dir),at3=path45.dirname(p);for(;!fs53.existsSync(at3);)at3=path45.dirname(at3);let real2=fs53.realpathSync(at3);return real2===home2||real2.startsWith(`${home2}${path45.sep}`)}catch{return!1}}function toolCacheEnv(dir){let at3=p=>path45.join(dir,p);return{XDG_CACHE_HOME:at3("xdg"),PIP_CACHE_DIR:at3("pip"),POETRY_CACHE_DIR:at3("poetry"),UV_CACHE_DIR:at3("uv"),PUB_CACHE:at3("pub"),COMPOSER_CACHE_DIR:at3("composer"),MAVEN_USER_HOME:at3("maven"),GOCACHE:at3("go-build"),DENO_DIR:at3("deno"),BUN_INSTALL_CACHE_DIR:at3("bun"),npm_config_devdir:at3("node-gyp"),PUPPETEER_CACHE_DIR:at3("puppeteer"),CYPRESS_CACHE_FOLDER:at3("cypress"),electron_config_cache:at3("electron")}}function installedEnv(){return{FLUTTER_ALREADY_LOCKED:"true"}}function noReportsEnv(){return{FLUTTER_SUPPRESS_ANALYTICS:"true",DOTNET_CLI_TELEMETRY_OPTOUT:"1"}}function pnpmStoreEnv(dir){return{pnpm_config_store_dir:path45.join(dir,"pnpm-store"),npm_config_store_dir:path45.join(dir,"pnpm-store")}}function npmCacheEnv(dir,home2){let npm=path45.join(dir,"npm");try{if(!inside3(dir,npm))return{};fs53.mkdirSync(npm,{recursive:!0});let shared=path45.join(home2,".npm","_cacache"),link=path45.join(npm,"_cacache");return fs53.existsSync(shared)&&!fs53.lstatSync(link,{throwIfNoEntry:!1})&&fs53.symlinkSync(shared,link),{npm_config_cache:npm}}catch{return{}}}function linkVersions(from,to,name){fs53.mkdirSync(to,{recursive:!0});for(let entry of fs53.existsSync(from)?fs53.readdirSync(from):[])if(name.test(entry))try{fs53.symlinkSync(path45.join(from,entry),path45.join(to,entry))}catch{}}function theirCaches(home2,env2,mac=process.platform==="darwin"){return mac?path45.join(home2,"Library","Caches"):env2.XDG_CACHE_HOME??path45.join(home2,".cache")}function browserCacheEnv(dir,home2,env2){let set2=env2.PLAYWRIGHT_BROWSERS_PATH;if(set2==="0"||process.platform==="win32")return{};let playwright=path45.join(dir,"ms-playwright");try{if(!inside3(dir,playwright))return{};linkVersions(set2?path45.resolve(set2):path45.join(theirCaches(home2,env2),"ms-playwright"),playwright,/^[a-z_]+-\d+$/);let puppeteer=path45.join(home2,".cache","puppeteer");for(let kind of fs53.existsSync(puppeteer)?fs53.readdirSync(puppeteer):[])/^chrome(-headless-shell)?$|^chromedriver$|^firefox$/.test(kind)&&linkVersions(path45.join(puppeteer,kind),path45.join(dir,"puppeteer",kind),/^[\w.-]+-[\w.]+$/);return{PLAYWRIGHT_BROWSERS_PATH:playwright}}catch{return{}}}var START_AS_COPY=[{mine:()=>path45.join("xdg","prisma"),theirs:(home2,env2)=>path45.join(env2.XDG_CACHE_HOME??path45.join(home2,".cache"),"prisma"),most:400*2**20}],LINKED_SETTINGS=[{mine:"gradle/gradle.properties",theirs:".gradle/gradle.properties"},{mine:"gradle/init.d",theirs:".gradle/init.d"},{mine:"maven/settings.xml",theirs:".m2/settings.xml"},{mine:"maven/settings-security.xml",theirs:".m2/settings-security.xml"},{mine:"maven/toolchains.xml",theirs:".m2/toolchains.xml"}];function sizeOf2(p,most){let total=0,walk3=at3=>{for(let e of fs53.readdirSync(at3,{withFileTypes:!0})){if(total>most)return;let full=path45.join(at3,e.name);e.isDirectory()?walk3(full):e.isFile()&&(total+=fs53.statSync(full).size)}};return walk3(p),total}function madeWhole(to,make){let part=`${to}.making-${process.pid}`;fs53.rmSync(part,{recursive:!0,force:!0});try{make(part),fs53.renameSync(part,to)}catch{fs53.rmSync(part,{recursive:!0,force:!0})}}function copyWhole(from,to){madeWhole(to,part=>fs53.cpSync(from,part,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0,mode:fs53.constants.COPYFILE_FICLONE}))}function startHome(dir,home2,env2){for(let c of START_AS_COPY){let to=path45.join(dir,c.mine(home2,env2)),from=c.theirs(home2,env2);try{if(fs53.lstatSync(to,{throwIfNoEntry:!1})||!fs53.existsSync(from)||!inside3(dir,to)||sizeOf2(from,c.most)>c.most)continue;fs53.mkdirSync(path45.dirname(to),{recursive:!0}),copyWhole(from,to)}catch{}}for(let s of LINKED_SETTINGS){let to=path45.join(dir,s.mine),from=path45.join(home2,s.theirs);try{if(fs53.lstatSync(to,{throwIfNoEntry:!1})||!fs53.existsSync(from)||!inside3(dir,to))continue;fs53.mkdirSync(path45.dirname(to),{recursive:!0}),fs53.symlinkSync(from,to)}catch{}}}function gradleWrapper(dir,cwd,home2,env2){let said="";try{said=fs53.readFileSync(path45.join(cwd,"gradle","wrapper","gradle-wrapper.properties"),"utf8")}catch{return}let value=key=>new RegExp(`^${key}\\s*=\\s*(.+)$`,"m").exec(said)?.[1]?.trim().replace(/\\(.)/g,"$1"),url=value("distributionUrl");if(!url||(value("distributionBase")??"GRADLE_USER_HOME")!=="GRADLE_USER_HOME"||(value("distributionPath")??"wrapper/dists")!=="wrapper/dists")return;let name=path45.basename(url).replace(/\.zip$/,""),hash2=BigInt(`0x${createHash2("md5").update(url).digest("hex")}`).toString(36),rel=path45.join("wrapper","dists",name,hash2),theirs=path45.join(env2.GRADLE_USER_HOME??path45.join(home2,".gradle"),rel),mine2=path45.join(dir,"gradle",rel),ok2=`${name}.zip.ok`;if(fs53.lstatSync(mine2,{throwIfNoEntry:!1})||!fs53.existsSync(path45.join(theirs,ok2))||!inside3(dir,mine2))return;let unpacked=fs53.readdirSync(theirs,{withFileTypes:!0}).filter(e=>e.isDirectory());unpacked.length===1&&(fs53.mkdirSync(path45.dirname(mine2),{recursive:!0}),madeWhole(mine2,part=>{fs53.mkdirSync(part),fs53.symlinkSync(path45.join(theirs,unpacked[0].name),path45.join(part,unpacked[0].name)),fs53.copyFileSync(path45.join(theirs,ok2),path45.join(part,ok2))}))}function theirsToRead(home2,env2,mavenOpts){let out18={},gradle=path45.join(env2.GRADLE_USER_HOME??path45.join(home2,".gradle"),"caches");!env2.GRADLE_RO_DEP_CACHE&&fs53.existsSync(path45.join(gradle,"modules-2"))&&(out18.GRADLE_RO_DEP_CACHE=gradle);let nuget=env2.NUGET_PACKAGES??path45.join(home2,".nuget","packages");fs53.existsSync(nuget)&&(out18.NUGET_FALLBACK_PACKAGES=[env2.NUGET_FALLBACK_PACKAGES?.trim(),nuget].filter(Boolean).join(";"));let maven=path45.join(home2,".m2","repository");return fs53.existsSync(maven)&&!/-Dmaven\.repo\.local\.tail=/.test(mavenOpts)&&(out18.MAVEN_OPTS=`${mavenOpts} -Dmaven.repo.local.tail=${maven}`),out18}function cacheHomeEnv(dir,o){let env2=o.env??process.env,home2=o.home??os8.homedir();try{ownFolder(dir),startHome(dir,home2,env2),o.cwd&&gradleWrapper(dir,o.cwd,home2,env2)}catch{}let packages=packageCacheEnv(dir,env2);return{...packages,...theirsToRead(home2,env2,packages.MAVEN_OPTS),...toolCacheEnv(dir),...installedEnv(),...noReportsEnv(),...npmCacheEnv(dir,home2),...browserCacheEnv(dir,home2,env2),...o.wall==="project"?pnpmStoreEnv(dir):{}}}var HEALED_LINE="A tool could not save its downloads in your home folder, so squad-kit gave it the project's own folder and ran it again.",NOT_A_CACHE=new Set([".config",".local",".Trash",".nvm",".rvm",".rbenv",".pyenv",".sdkman",".rustup",".volta",".asdf",".jenv",".nodenv",".goenv",".fnm"]),ON_JAVA=new Set([".gradle",".m2",".android",".kotlin",".konan",".sbt",".ivy2",".java",".skiko"]);function homeCachePlace(target,home2,hidden){let rel=path45.relative(home2,target);if(!rel||rel.startsWith("..")||path45.isAbsolute(rel))return null;let parts2=rel.split(path45.sep),top=parts2[0],depth=top===".cache"?2:top==="Library"&&parts2[1]==="Caches"?3:top.startsWith(".")&&!NOT_A_CACHE.has(top)?1:0;if(!depth)return null;let place=path45.join(home2,...parts2.slice(0,Math.min(depth,parts2.length))),near=(a,b)=>a===b||a.startsWith(`${b}${path45.sep}`);return hidden.some(h=>near(place,h)||near(h,place))?null:place}function realFolderAt(p){let at3=fs53.lstatSync(p,{throwIfNoEntry:!1});at3?.isDirectory()||(at3&&fs53.rmSync(p,{force:!0}),fs53.mkdirSync(p))}function linkEntries(from,to,skip,hidden){let names2=[];try{names2=fs53.readdirSync(from)}catch{return}for(let name of names2){let theirs=path45.join(from,name);if(skip.has(name)||hidden.has(theirs))continue;let mine2=path45.join(to,name),at3=fs53.lstatSync(mine2,{throwIfNoEntry:!1});at3&&!at3.isSymbolicLink()||at3&&fs53.readlinkSync(mine2)===theirs||(at3&&fs53.rmSync(mine2,{force:!0}),fs53.symlinkSync(theirs,mine2))}}var COPY_MOST=200*2**20;function ownPlace(from,to,hidden){let at3=fs53.lstatSync(to,{throwIfNoEntry:!1});if(at3&&!at3.isSymbolicLink())return;at3&&fs53.rmSync(to,{force:!0});let theirs=fs53.statSync(from,{throwIfNoEntry:!1});if(theirs){if(theirs.isFile())return fs53.copyFileSync(from,to);if(theirs.isDirectory()){if(sizeOf2(from,COPY_MOST)<=COPY_MOST)return copyWhole(from,to);fs53.mkdirSync(to),linkEntries(from,to,new Set,hidden)}}}function healHome(dir,places,o){let own2=path45.join(dir,"home");ownFolder(dir),realFolderAt(own2);let hidden=new Set(o.hidden),ways=new Map;for(let place of places){let parts2=path45.relative(o.home,place).split(path45.sep);parts2.forEach((part,i)=>{let at3=parts2.slice(0,i).join(path45.sep);ways.set(at3,(ways.get(at3)??new Set).add(part))})}let placed=new Set(places.map(p=>path45.relative(o.home,p))),walk3=rel=>{let mine2=path45.join(own2,rel);linkEntries(path45.join(o.home,rel),mine2,ways.get(rel)??new Set,hidden);for(let name of ways.get(rel)??[]){let next=rel?path45.join(rel,name):name;placed.has(next)?ownPlace(path45.join(o.home,next),path45.join(own2,next),hidden):(realFolderAt(path45.join(own2,next)),walk3(next))}};return walk3(""),own2}function healEnv(dir,places,o){let home2=o.home??os8.homedir(),own2=healHome(dir,places,{home:home2,hidden:o.hidden}),java=places.some(p=>ON_JAVA.has(path45.relative(home2,p).split(path45.sep)[0])),opts=(o.env??process.env).JAVA_TOOL_OPTIONS?.trim();return{HOME:own2,...java?{JAVA_TOOL_OPTIONS:[opts,`-Duser.home=${own2}`].filter(Boolean).join(" ")}:{}}}function learnedFile(dir){return`${dir}.healed.json`}function healedBefore(dir,command,o){let home2=o.home??os8.homedir();try{let said=JSON.parse(fs53.readFileSync(learnedFile(dir),"utf8"))[command];return Array.isArray(said)?said.filter(p=>typeof p=="string"&&homeCachePlace(p,home2,o.hidden)===p):[]}catch{return[]}}function rememberHealed(dir,command,places){let file3=learnedFile(dir),all={};try{all=JSON.parse(fs53.readFileSync(file3,"utf8"))}catch{}all[command]=[...new Set(places)];try{fs53.mkdirSync(path45.dirname(file3),{recursive:!0}),fs53.writeFileSync(file3,`${JSON.stringify(all,null,2)}
@@ -601,7 +601,8 @@ $sk$;
601
601
  ${text3};`,`Reading ${path76.basename(a.name)}`),making2=startFolder(o.folder);try{let cat=JSON.parse((await read6(CATALOG3.replace(/;$/,"")))[0]),shadow=await shadowTables(a.name,cat),kept=cat.tables.filter(t=>!shadow.has(t.name)),tables=kept.map(t=>({name:t.name,ref:`src.${q3(t.name)}`,columns:t.columns.map(c=>({name:c.name,type:c.type,...c.hidden?{generated:!0}:{}})),pk:t.columns.filter(c=>c.pk>0).sort((x,y)=>x.pk-y.pk).map(c=>c.name)})),fks=[];for(let t of kept){let ids=[...new Set(t.fks.map(f=>f.id))];for(let id2 of ids){let parts2=t.fks.filter(f=>f.id===id2).sort((x,y)=>x.seq-y.seq),parent=tables.find(x=>x.name===parts2[0].parent),refCols=parts2.map((f,i)=>f.to??parent?.pk[i]??"");refCols.every(Boolean)&&fks.push({child:t.name,cols:parts2.map(f=>f.from),parent:parts2[0].parent,refCols})}}let plan=await planRows(tables,fks,dialect3,read6,o),counts3=await countKept(plan,dialect3,read6),faker=o.hidePersonal?makeFaker():null,hidden=[],lines3=["PRAGMA foreign_keys = OFF;",attach(a.name),"BEGIN;"],ordered=[...kept].sort((x,y)=>Number(/^\s*create\s+virtual/i.test(y.sql))-Number(/^\s*create\s+virtual/i.test(x.sql)));for(let t of ordered)lines3.push(`${t.sql.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i,"CREATE TABLE IF NOT EXISTS ")};`);for(let t of tables){let cols=t.columns.filter(c=>!c.generated),list2=cols.map(c=>q3(c.name)).join(", "),hide=new Map;if(faker&&cols.forEach((c,i)=>{let kind=personalKind(c.name,t.name);kind&&fits(kind,typeClass(c.type))&&(hide.set(i,kind),hidden.push(`${t.name}.${c.name}`))}),!hide.size){lines3.push(`INSERT INTO main.${q3(t.name)} (${list2}) ${selectKept(plan,dialect3,t,list2)};`);continue}let rows2=await read6(selectKept(plan,dialect3,t,dialect3.jsonArray(cols.map(c=>`quote(${q3(c.name)})`))));for(let row3 of rows2){let lits=JSON.parse(row3).map((l,i)=>hide.has(i)?fakeLiteral(l,hide.get(i),faker):l);lines3.push(`INSERT INTO main.${q3(t.name)} (${list2}) VALUES (${lits.join(", ")});`)}}cat.sequence&&lines3.push("DELETE FROM main.sqlite_sequence;","INSERT INTO main.sqlite_sequence SELECT * FROM src.sqlite_sequence;");for(let kind of["index","view","trigger"])for(let x of cat.other.filter(y=>y.type===kind))lines3.push(`${x.sql};`);lines3.push("COMMIT;","DETACH src;","VACUUM;");let file3=path76.join(making2,TEMPLATE_FILE(a.name));await run2(file3,lines3.join(`
602
602
  `)+`
603
603
  `,`Making the template of ${path76.basename(a.name)}`);let notes=[...plan.notes];shadow.size&&notes.push("Search indexes kept by virtual tables are made again from the kept rows.");let info2=finishFolder(making2,o.folder,{kind:"sqlite",source:a.safe,sourceName:path76.basename(a.name),madeAt:new Date().toISOString(),smallRows:o.smallRows,sampleRows:o.sampleRows,hidePersonal:o.hidePersonal,tables:describeTables(plan,counts3),hidden,linksComplete:plan.complete,notes});return{kind:"sqlite",folder:o.folder,source,info:info2}}catch(e){throw dropFolder(making2),e}},async createStoryDb(template,story2){let a=readAddress("sqlite",template.source),file3=storyFile(a.name,story2),address=a.withName(file3);refuseOwn("sqlite",address,template.source,"makes or changes");let real2=realFile(file3);if(fs81.existsSync(file3)){let mark=readMark("sqlite","sqlite",real2);if(mark?.role==="story"&&mark.story===story2.id.trim())return{kind:"sqlite",name:file3,address,existed:!0};throw new DbError(`A file named ${path76.basename(file3)} is already in the story\u2019s folder, and squad-kit did not make it for this story. squad-kit leaves it alone.`,"not-ours")}if(!readTemplate(template.folder,template.source,"sqlite"))throw new DbError("The template\u2019s files are not on this computer any more. Make the template again.","failed");return fs81.mkdirSync(path76.dirname(file3),{recursive:!0}),fs81.copyFileSync(path76.join(template.folder,TEMPLATE_FILE(a.name)),file3,fs81.constants.COPYFILE_EXCL),putMark({kind:"sqlite",server:"sqlite",name:realFile(file3),role:"story",story:story2.id.trim(),sourceName:path76.basename(a.name),madeAt:new Date().toISOString()}),{kind:"sqlite",name:file3,address,existed:!1}},async dropStoryDb(address,personsAddress2){refuseOwn("sqlite",address,personsAddress2,"drops");let a=readAddress("sqlite",address),p=readAddress("sqlite",personsAddress2),real2=realFile(a.name),mark=readMark("sqlite","sqlite",real2);if(!fs81.existsSync(a.name))return mark?.role==="story"&&dropMark("sqlite","sqlite",real2),!1;let ext=path76.extname(p.name);if(mark?.role!=="story"||!mark.story||path76.basename(a.name)!==`${storyName(path76.basename(p.name,ext),mark.story)}${ext||".db"}`)throw new DbError(`squad-kit did not make ${path76.basename(a.name)} for a story of this project, so it leaves it alone.`,"not-ours");for(let f of[a.name,`${a.name}-wal`,`${a.name}-shm`,`${a.name}-journal`])fs81.rmSync(f,{force:!0});return dropMark("sqlite","sqlite",real2),!0},async removeTemplate(template){fs81.rmSync(template.folder,{recursive:!0,force:!0})}};var ADAPTERS={sqlite,postgres,mysql,mongo};function dbAdapter(kind){let a=ADAPTERS[kind];if(!a)throw new Error(`squad-kit has no adapter for ${String(kind)} databases.`);return a}function readTemplate2(folder,source){return readTemplate(folder,source)}import fs82 from"fs";import path77 from"path";import yaml12 from"js-yaml";var SKIP_DIRS2=new Set(["node_modules",".git",".hg",".svn",".squad","dist","build","out",".next",".nuxt",".svelte-kit",".output",".turbo",".cache","coverage",".venv","venv","__pycache__","target","vendor","Pods",".gradle",".idea",".vscode","bin","obj","wwwroot"]),LIMITS2={depth:6,entries:8e3,fileBytes:256*1024,code:600},COMPOSE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,ENV_FILE=/^\.env(?:\.[\w.-]+)?$/,EXAMPLE=/(?:^|\.)(?:example|sample|template|dist|defaults|tpl)$/i,APPSETTINGS=/^appsettings(?:\.[\w-]+)?\.json$/i,SPRING=/^application(?:-[\w-]+)?\.(?:properties|ya?ml)$/,CODE3=/\.(?:m?[jt]sx?|c[jt]s|py|cs)$/i,DB_ENV=/^(?:[A-Z0-9]+_)*(?:DATABASE|DB|MONGO|MONGODB|POSTGRES|POSTGRESQL|PG|MYSQL|MARIADB|SQLITE)(?:_[A-Z0-9]+)*_(?:URL|URI|DSN|CONNECTION|CONNECTION_STRING|CONN|CONN_STRING|PATH|FILE)$/,NOT_THE_APPS=/(?:^|_)(?:SHADOW|TEST|TESTS|TESTING|CI|E2E|READ|REPLICA|DIRECT)(?:_|$)/,SCHEMES={postgres:"postgres",postgresql:"postgres",postgis:"postgres",mysql:"mysql",mysql2:"mysql",mysqlx:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",sqlite:"sqlite",sqlite3:"sqlite",file:"sqlite",sqlserver:"sqlserver",mssql:"sqlserver"},unquote3=s=>s.trim().replace(/^(['"`])(.*)\1$/s,"$2").trim();function pairsOf(address){if(!/^[\w\s]+=/.test(address)||/^[a-z][\w+.-]*:\/\//i.test(address))return null;let out18=new Map;for(let part of address.split(";")){let at3=part.indexOf("=");at3>0&&out18.set(part.slice(0,at3).trim().toLowerCase(),part.slice(at3+1).trim())}return out18.size?out18:null}var SQLITE_FILE=/\.(?:db|sqlite3?|db3)$/i;function kindOf(address){let a=unquote3(address),scheme=/^(?:jdbc:)?([a-z][a-z0-9+.-]*):/i.exec(a)?.[1]?.toLowerCase();if(scheme&&SCHEMES[scheme])return SCHEMES[scheme];let pairs=pairsOf(a);if(pairs){let has3=(...keys)=>keys.some(k=>pairs.has(k));return SQLITE_FILE.test(pairs.get("data source")??pairs.get("filename")??"")?"sqlite":has3("trusted_connection","integrated security","initial catalog","encrypt","trustservercertificate","multipleactiveresultsets")?"sqlserver":has3("host")&&has3("database","username")?"postgres":has3("uid","pwd")||pairs.get("port")==="3306"?"mysql":null}return SQLITE_FILE.test(a)&&!/\s/.test(a)?"sqlite":null}function readAddress2(address){let a=unquote3(address).replace(/^jdbc:/i,""),file3=/^(?:file|sqlite3?):(?:\/\/)?(.+)$/i.exec(a);if(file3)return{name:file3[1].split("?")[0],password:!1};let link=/^[a-z][\w+.-]*:\/\/(?:([^:@/]*)(?::([^@/]*))?@)?([^/?#]*)(?:\/([^?#]*))?/i.exec(a);if(link){let first2=(link[3]??"").split(",")[0]??"",[host,port]=first2.startsWith("[")?[first2.slice(0,first2.indexOf("]")+1),first2.slice(first2.indexOf("]")+2)]:first2.split(":"),name=decodeURIComponent((link[4]??"").split("/")[0]??"");return{...name?{name}:{},...host?{host}:{},...port?{port}:{},password:!!link[2]}}let pairs=pairsOf(a);if(pairs){let name=pairs.get("database")??pairs.get("initial catalog")??pairs.get("dbname")??(SQLITE_FILE.test(pairs.get("data source")??"")?pairs.get("data source"):void 0)??pairs.get("filename"),server=pairs.get("host")??pairs.get("server")??pairs.get("address")??pairs.get("addr"),[host,port]=(server??"").replace(/^tcp:/i,"").split(/[:,]/);return{...name?{name}:{},...host?{host}:{},...pairs.get("port")??port?{port:pairs.get("port")??port}:{},password:!!(pairs.get("password")??pairs.get("pwd"))}}return SQLITE_FILE.test(a)?{name:a,password:!1}:{password:!1}}function withoutPassword(address){return address.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:)([^@/]*)@/i,(_m,head,pass)=>pass?`${head}***@`:`${head}@`).replace(/((?:^|;)\s*(?:password|pwd)\s*=)([^;]*)/gi,"$1***")}function hasPassword(address){return readAddress2(address).password}function filesOf2(dir){let out18={prisma:[],compose:[],env:[],appsettings:[],spring:[],csproj:[],packages:[],code:[]},entries=0,visit=(rel,depth)=>{if(depth>LIMITS2.depth||entries>=LIMITS2.entries)return;let list2;try{list2=fs82.readdirSync(path77.join(dir,rel),{withFileTypes:!0})}catch{return}if(!list2.some(e=>e.name==="pyvenv.cfg"))for(let e of list2){if(++entries>=LIMITS2.entries)return;let p=rel?`${rel}/${e.name}`:e.name;if(e.isDirectory()){!SKIP_DIRS2.has(e.name)&&!e.name.startsWith(".")&&visit(p,depth+1);continue}e.isFile()&&(e.name.endsWith(".prisma")?out18.prisma.push(p):COMPOSE.test(e.name)?out18.compose.push(p):ENV_FILE.test(e.name)&&depth<=3?out18.env.push(p):APPSETTINGS.test(e.name)?out18.appsettings.push(p):SPRING.test(e.name)&&(rel.endsWith("resources")||rel==="config"||rel==="")?out18.spring.push(p):e.name.endsWith(".csproj")?out18.csproj.push(p):e.name==="package.json"&&depth<=3?out18.packages.push(p):CODE3.test(e.name)&&out18.code.length<LIMITS2.code&&out18.code.push(p))}};return visit("",0),out18}function read2(file3){try{return fs82.statSync(file3).size>LIMITS2.fileBytes?"":fs82.readFileSync(file3,"utf8")}catch{return""}}var PLACEHOLDER=/\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*/;function envPairs(text3){let out18=new Map;for(let line3 of text3.split(/\r?\n/)){let m=/^\s*(?:export\s+)?([A-Za-z_][\w.]*)\s*=\s*(.*?)\s*$/.exec(line3);if(!m)continue;let value=m[2];/^['"]/.test(value)||(value=value.replace(/\s+#.*$/,"")),out18.set(m[1],unquote3(value))}return out18}function composeValue(v){if(v==null)return;let s=String(v),m=/^\$\{[A-Za-z_]\w*:?-([^}]*)\}$/.exec(s);return m?m[1]:PLACEHOLDER.test(s)?void 0:s}function composeEnv(env2){let out18=new Map;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&out18.set(s.slice(0,at3),composeValue(s.slice(at3+1)))}else if(env2&&typeof env2=="object")for(let[k,v]of Object.entries(env2))out18.set(k,composeValue(v));return out18}var IMAGE_KIND=[[/(?:^|\/)(?:postgres|postgresql|postgis|timescaledb[\w-]*)(?::|$)/i,"postgres"],[/(?:^|\/)(?:mysql|mariadb|percona)(?::|$)/i,"mysql"],[/(?:^|\/)(?:mongo|mongodb(?:-community-server)?)(?::|$)/i,"mongo"],[/mssql|sql-?server|azure-sql-edge/i,"sqlserver"]],KIND_PORT={postgres:5432,mysql:3306,mongo:27017,sqlite:0,sqlserver:1433};function published(ports,target){for(let p of Array.isArray(ports)?ports:[]){if(p&&typeof p=="object"){let o=p;if(Number(o.target)===target&&o.published!==void 0)return String(o.published);continue}let parts2=String(p).split("/")[0].split(":"),inside5=Number(parts2.at(-1));if(inside5===target)return parts2.length>=2?parts2.at(-2):String(inside5)}}function composeDbs(dir,files){let dbs=[],settings=[];for(let file3 of files){let doc;try{doc=yaml12.load(read2(path77.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let s=raw??{},env2=composeEnv(s.environment),kind=IMAGE_KIND.find(([re])=>re.test(String(s.image??"")))?.[1];if(!kind){for(let[k,v]of env2)DB_ENV.test(k)&&!NOT_THE_APPS.test(k)&&settings.push({file:file3,env:k,...v?{value:v}:{}});continue}let port=published(s.ports,KIND_PORT[kind]),get=(...keys)=>keys.map(k=>env2.get(k)).find(v=>v),at3=`localhost:${port??KIND_PORT[kind]}`,userPass=(user,pass)=>user?`${encodeURIComponent(user)}${pass?`:${encodeURIComponent(pass)}`:""}@`:"",name,address;if(kind==="postgres"){let user=get("POSTGRES_USER")??"postgres";name=get("POSTGRES_DB")??user,address=`postgres://${userPass(user,get("POSTGRES_PASSWORD"))}${at3}/${name}`}else if(kind==="mysql"){let user=get("MYSQL_USER","MARIADB_USER")??"root",pass=user==="root"?get("MYSQL_ROOT_PASSWORD","MARIADB_ROOT_PASSWORD"):get("MYSQL_PASSWORD","MARIADB_PASSWORD");name=get("MYSQL_DATABASE","MARIADB_DATABASE"),name&&(address=`mysql://${userPass(user,pass)}${at3}/${name}`)}else if(kind==="mongo"){name=get("MONGO_INITDB_DATABASE");let user=get("MONGO_INITDB_ROOT_USERNAME");name&&(address=`mongodb://${userPass(user,get("MONGO_INITDB_ROOT_PASSWORD"))}${at3}/${name}${user?"?authSource=admin":""}`)}dbs.push({file:file3,service,kind,...name?{name}:{},...port?{port}:{},...address?{address}:{}})}}return{dbs,settings}}var PROVIDER={postgresql:"postgres",postgres:"postgres",mysql:"mysql",sqlite:"sqlite",mongodb:"mongo",sqlserver:"sqlserver",cockroachdb:"postgres"};function prismaSources(dir,files){let out18=[];for(let file3 of files){let text3=read2(path77.join(dir,file3)),block3=/datasource\s+\w+\s*\{([^}]*)\}/.exec(text3)?.[1];if(!block3)continue;let provider=/provider\s*=\s*"(\w+)"/.exec(block3)?.[1],env2=/url\s*=\s*env\(\s*"([^"]+)"\s*\)/.exec(block3)?.[1],url=/url\s*=\s*"([^"]+)"/.exec(block3)?.[1];out18.push({file:file3,...provider&&PROVIDER[provider]?{kind:PROVIDER[provider]}:{},...env2?{env:env2}:{},...url?{url}:{}})}return out18}function looseJson(text3){let bare=text3.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(m,str4)=>str4??"").replace(/,(\s*[}\]])/g,"$1");return JSON.parse(bare)}function appsettingsKeys(dir,file3){let doc;try{doc=looseJson(read2(path77.join(dir,file3)))}catch{return[]}let out18=[],strings2=doc?.ConnectionStrings;if(strings2&&typeof strings2=="object")for(let[k,v]of Object.entries(strings2))typeof v=="string"&&v.trim()&&out18.push({file:file3,key:`ConnectionStrings:${k}`,value:v});let walk3=(o,at3,depth)=>{if(!(!o||typeof o!="object"||Array.isArray(o)||depth>2)){for(let[k,v]of Object.entries(o))if(!(!at3.length&&k==="ConnectionStrings"))if(typeof v=="string"&&kindOf(v)&&/:\/\/|=/.test(v)){let named=Object.entries(o).find(([n,x])=>/^(?:database(?:name)?|dbname)$/i.test(n)&&typeof x=="string")?.[1];out18.push({file:file3,key:[...at3,k].join(":"),value:v,...named?{name:named}:{}})}else walk3(v,[...at3,k],depth+1)}};return walk3(doc,[],0),out18}var SPRING_KEYS=["spring.datasource.url","spring.data.mongodb.uri","spring.r2dbc.url"];function springKeys(dir,file3){let text3=read2(path77.join(dir,file3)),flat=new Map;if(file3.endsWith(".properties"))for(let line3 of text3.split(/\r?\n/)){let m=/^\s*([\w.-]+)\s*[=:]\s*(.*?)\s*$/.exec(line3);m&&!line3.trim().startsWith("#")&&flat.set(m[1],m[2])}else{let doc;try{doc=yaml12.load(text3)}catch{return[]}let walk3=(o,at3)=>{if(!(!o||typeof o!="object"||Array.isArray(o)))for(let[k,v]of Object.entries(o)){let key=at3?`${at3}.${k}`:k;v&&typeof v=="object"?walk3(v,key):v!=null&&flat.set(key,String(v))}};walk3(doc,"")}let out18=[];for(let key of SPRING_KEYS){let raw=flat.get(key);if(!raw)continue;let holder=/^\$\{([A-Za-z_][\w.]*)(?::([^}]*))?\}$/.exec(raw),value=holder?holder[2]??"":raw,user=flat.get("spring.datasource.username"),pass=flat.get("spring.datasource.password"),db=flat.get("spring.data.mongodb.database");out18.push({file:file3,key,value,...holder?{env:holder[1]}:{},...user&&!PLACEHOLDER.test(user)?{user}:{},...pass&&!PLACEHOLDER.test(pass)?{pass}:{},...db?{db}:{}})}return out18}var PACKAGE_KIND=[[/^(?:mongoose|mongodb|@nestjs\/mongoose|mongodb\.driver|mongodb\.entityframeworkcore|mongoengine|pymongo|motor)$/i,"mongo"],[/^(?:pg|postgres|pg-promise|@neondatabase\/serverless|npgsql(?:\.entityframeworkcore\.postgresql)?|psycopg2?(?:-binary)?|asyncpg)$/i,"postgres"],[/^(?:mysql|mysql2|pomelo\.entityframeworkcore\.mysql|mysql\.entityframeworkcore|mysqlconnector|pymysql|mysqlclient)$/i,"mysql"],[/^(?:better-sqlite3|sqlite3|sqlite|microsoft\.entityframeworkcore\.sqlite|microsoft\.data\.sqlite)$/i,"sqlite"],[/^(?:microsoft\.entityframeworkcore\.sqlserver|microsoft\.data\.sqlclient|system\.data\.sqlclient|mssql|tedious)$/i,"sqlserver"]];function packageKinds(dir,files){let names2=[];for(let f of files.packages)try{let p=JSON.parse(read2(path77.join(dir,f)));names2.push(...Object.keys({...p.dependencies,...p.devDependencies}))}catch{}for(let f of files.csproj)for(let m of read2(path77.join(dir,f)).matchAll(/<PackageReference\s+Include="([^"]+)"/g))names2.push(m[1]);let kinds=[];for(let n of names2){let k=PACKAGE_KIND.find(([re])=>re.test(n))?.[1];k&&!kinds.includes(k)&&kinds.push(k)}return kinds}var ENV_READS=[/process\.env\.([A-Z][A-Z0-9_]*)/g,/process\.env\[\s*['"`]([A-Z][A-Z0-9_]*)['"`]\s*\]/g,/(?:os\.environ\.get|os\.getenv|environ\.get|Deno\.env\.get|Environment\.GetEnvironmentVariable)\(\s*['"]([A-Z][A-Z0-9_]*)['"]/g,/os\.environ\[\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\]/g];function codeReads(dir,files){let env2=new Set,connection=new Set;for(let f of files){let text3=read2(path77.join(dir,f));if(text3){for(let re of ENV_READS)for(let m of text3.matchAll(re))(DB_ENV.test(m[1])||/MONGO/.test(m[1]))&&env2.add(m[1]);for(let m of text3.matchAll(/GetConnectionString\(\s*"([^"]+)"\s*\)/g))connection.add(m[1])}}return{env:env2,connection}}function realEnvFile(file3){let dir=path77.posix.dirname(file3),base2=path77.posix.basename(file3).replace(/\.(?:example|sample|template|dist|defaults|tpl)$/i,"");return dir==="."?base2:`${dir}/${base2}`}function findIn(root,repo){let dir=path77.join(root,repo),files=filesOf2(dir),prisma=prismaSources(dir,files.prisma),compose=composeDbs(dir,files.compose),kinds=packageKinds(dir,files),reads=codeReads(dir,files.code),env2=files.env.map(f=>({file:f,example:EXAMPLE.test(f),pairs:envPairs(read2(path77.join(dir,f)))})),envValue2=name2=>{let hits=env2.filter(e=>e.pairs.has(name2)&&e.pairs.get(name2)&&!PLACEHOLDER.test(e.pairs.get(name2))),best2=hits.find(e=>!e.example)??hits[0];return best2?{file:best2.file,value:best2.pairs.get(name2),example:best2.example}:void 0},envFileOf=name2=>{let own3=env2.find(e=>!e.example&&e.pairs.has(name2))??env2.find(e=>e.pairs.has(name2));return own3?realEnvFile(own3.file):".env"},candidates=[];for(let p of prisma)if(p.env){let v=envValue2(p.env);candidates.push({setting:{env:p.env,file:envFileOf(p.env)},...v?{address:v.value}:{},...p.kind?{kind:p.kind}:{},from:[p.file,...v?[v.file]:[]],score:50})}else p.url&&candidates.push({setting:{file:p.file,key:"datasource url"},address:p.url,...p.kind?{kind:p.kind}:{},from:[p.file],score:35});let dotnet=[...files.appsettings].sort((a,b)=>Number(/development/i.test(b))-Number(/development/i.test(a))||a.length-b.length);for(let f of dotnet)for(let k of appsettingsKeys(dir,f)){let asked=[...reads.connection].some(n=>k.key===`ConnectionStrings:${n}`),kind2=kindOf(k.value)??void 0,name2=k.name??readAddress2(k.value).name;candidates.push({setting:{file:f,key:k.key,env:k.key.split(":").join("__")},address:k.value,...kind2?{kind:kind2}:{},...name2?{name:name2}:{},from:[f],score:40+(asked?5:0)+(/development/i.test(f)?2:0)})}for(let f of files.spring)for(let k of springKeys(dir,f)){let envName=k.env??k.key.toUpperCase().replace(/[.-]/g,"_"),address2=k.value||void 0;address2&&k.user&&!readAddress2(address2).password&&(address2=address2.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/)/i,`$1${encodeURIComponent(k.user)}${k.pass?`:${encodeURIComponent(k.pass)}`:""}@`));let kind2=address2?kindOf(address2)??void 0:void 0;candidates.push({setting:{file:f,key:k.key,env:envName},...address2?{address:address2}:{},...kind2?{kind:kind2}:{},...k.db?{name:k.db}:{},from:[f],score:40+(/(?:^|\/)application\.(?:properties|ya?ml)$/.test(f)?2:0)})}let named=new Set;for(let e of env2)for(let[k,v]of e.pairs)(DB_ENV.test(k)&&!NOT_THE_APPS.test(k)||kindOf(v)&&/:\/\//.test(v)&&!NOT_THE_APPS.test(k))&&named.add(k);for(let k of named){if(candidates.some(c=>c.setting.env===k))continue;let v=envValue2(k),kind2=v?kindOf(v.value)??void 0:void 0;candidates.push({setting:{env:k,file:envFileOf(k)},...v?{address:v.value}:{},...kind2?{kind:kind2}:{},from:v?[v.file]:env2.filter(e=>e.pairs.has(k)).map(e=>e.file),score:25+(reads.env.has(k)?15:0)+(v?5:0)})}for(let k of reads.env)candidates.some(c=>c.setting.env===k)||NOT_THE_APPS.test(k)||candidates.push({setting:{env:k,file:".env"},from:[],score:22});for(let s of compose.settings){if(candidates.some(c=>c.setting.env===s.env)){candidates.find(c=>c.setting.env===s.env).from.push(s.file);continue}candidates.push({setting:{env:s.env,file:".env"},...s.value&&kindOf(s.value)?{kind:kindOf(s.value)}:{},from:[s.file],score:20})}let db=compose.dbs[0];if(!candidates.length&&!db)return null;let best=candidates.sort((a,b)=>b.score-a.score)[0]??{setting:{},from:[],score:10},kind=(best.address?kindOf(best.address):null)??best.kind??prisma.find(p=>p.kind)?.kind??db?.kind??kinds[0];if(!kind)return null;let own2=db&&db.kind===kind?db:void 0,address=best.address&&!(own2&&readAddress2(best.address).host===own2.service)?best.address:own2?.address??best.address,said=address?readAddress2(address):{password:!1,name:void 0,host:void 0,port:void 0},name=best.name??said.name??own2?.name,where=own2?`the ${own2.service} service in ${own2.file}${own2.port?`, on port ${own2.port}`:""}`:kind==="sqlite"?"a file in the project":said.host?`${said.host}${said.port?`:${said.port}`:""}`:void 0,from=[...new Set([...best.from,...own2?[own2.file]:[]])];return{...best,kind,...address?{address}:{},...name?{name}:{},...where?{where}:{},from,repo}}function findDatabase(root,repos){let best=repos.map(r=>findIn(root,r)).filter(x=>!!x).sort((a,b)=>b.score-a.score)[0];if(!best)return{found:null};if(best.kind==="sqlserver")return{found:null,later:"This project uses SQL Server. A database of its own for each story comes for SQL Server in a later version; until then stories use it as before."};let{score:_score,kind,...rest}=best;return{found:{...rest,kind}}}var CODE_GLOBS=["*.cs","*.fs","*.vb","*.ts","*.tsx","*.mts","*.cts","*.js","*.jsx","*.mjs","*.cjs","*.py","*.java","*.kt","*.scala","*.go","*.rb","*.php","*.rs","*.ex","*.exs","*.dart","*.swift"],OPENS=/(?:(?:\b(?:GetDatabase|getDatabase|get_database|getSiblingDB|useDb)|\bclient\.db|\.Database)\(\s*|\bdbName\s*[:=]\s*)(["'`])([^"'`]+)\1/,OPENS_GREP="((GetDatabase|getDatabase|get_database|getSiblingDB|useDb|client\\.db|\\.Database)\\([[:space:]]*|dbName[[:space:]]*[:=][[:space:]]*)[\"'`]",DB_WORDS=/data ?base|db|mongo|catalog|schema|connect|client/i,COMMON_NAMES=new Set(["postgres","postgresql","mysql","mariadb","mongo","mongodb","test","admin","app","db","database","default","main","local","dev"]),TEST_PATH=/(?:^|\/)(?:tests?|__tests__|spec|specs|e2e)\/|[.-](?:test|spec)\.[a-z]+$|Tests?\.(?:cs|fs|java|kt)$|(?:^|\/)test_[^/]*\.py$|_test\.(?:py|go)$/i,SERVER_KINDS=["postgres","mysql","mongo"],GREP_MS=2e4;function grepLines(dir,args){return git(dir,["grep","-n","-I",...args,"--",...CODE_GLOBS],{allowFail:!0,timeoutMs:GREP_MS}).split(`
604
- `).map(l=>/^(.+?):(\d+):(.*)$/.exec(l)).filter(m=>!!m).map(m=>({file:m[1],line:m[2],text:m[3]}))}var escapeRe=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function composeSets(dir,files,name){let passes=new RegExp(`^\\$(?:\\{${escapeRe(name)}(?::?-[^}]*)?\\}|${escapeRe(name)})$`);for(let file3 of files){let doc;try{doc=yaml12.load(read2(path77.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let env2=raw?.environment,value;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&s.slice(0,at3)===name&&(value=s.slice(at3+1))}else env2&&typeof env2=="object"&&(value=env2[name]);if(!(value==null||!String(value).trim())&&!passes.test(String(value).trim()))return{file:file3,service}}}}function storyCopyIgnored(dir,db,address){if(!SERVER_KINDS.includes(db.kind))return;let setting=db.setting.env??db.setting.key??"its setting",takes=`squad-kit gives each story its own copy when the app takes the database's name from ${setting}.`,name=db.name??(address?readAddress2(address).name:void 0);if(name&&!COMMON_NAMES.has(name.toLowerCase())){let quoted4=[`"${name}"`,`'${name}'`,`\`${name}\``],reads=text3=>text3.replace(/\bGetConnectionString\s*\(\s*(["'`])[^"'`]*\1\s*\)|\bConnectionStrings[:_]+\w+/g,""),hits=grepLines(dir,["-F",...quoted4.flatMap(q4=>["-e",q4])]).map(h=>({...h,text:reads(h.text)})).filter(h=>quoted4.some(q4=>h.text.includes(q4))&&DB_WORDS.test(quoted4.reduce((t,q4)=>t.split(q4).join(""),h.text))),at3=h=>`${h.file}:${h.line}`;if(hits.length)return`The code names the database ${name} itself, in ${hits.length===1?at3(hits[0]):`${hits.length} places like ${at3(hits[0])}`}, so it would not use a story's own copy under another name. ${takes}`}for(let h of grepLines(dir,["-E","-e",OPENS_GREP])){let other=TEST_PATH.test(h.file)?void 0:OPENS.exec(h.text)?.[2];if(other)return`The code opens the database ${other} by its name, in ${h.file}:${h.line}, so it would not use a story's own copy under another name. ${takes}`}let files=filesOf2(dir),values=[];if(db.setting.env)for(let f of files.env){let v=envPairs(read2(path77.join(dir,f))).get(db.setting.env);v&&values.push({value:v,file:f})}address&&values.push({value:address,file:""});for(let{value,file:file3}of values)if(!(!(kindOf(value)||/:\/\//.test(value))||readAddress2(value).name))return`The address in ${file3||setting} names no database, so the app picks its database some other way and would not use a story's own copy. ${takes}`;if(db.setting.env){let set2=composeSets(dir,files.compose,db.setting.env);if(set2)return`${set2.file} sets ${db.setting.env} itself for the ${set2.service} service, from other settings, so the app there would not see a story's own database.`}}var OWN_DB_WORDS="The story has its own copy of the database.",DB_KINDS=["postgres","mysql","mongo","sqlite"],KIND_NAME={postgres:"Postgres",mysql:"MySQL",mongo:"Mongo",sqlite:"SQLite"},dbDir=bp=>path78.join(bp.localDir,"database"),templateFolder=bp=>path78.join(dbDir(bp),"template"),stateFile2=bp=>path78.join(dbDir(bp),"state.json");function readState(bp){try{let s=JSON.parse(fs83.readFileSync(stateFile2(bp),"utf8"));return{...s,stories:Array.isArray(s.stories)?s.stories:[],notes:Array.isArray(s.notes)?s.notes:[]}}catch{return{stories:[],notes:[]}}}function changeState(bp,change){let s=readState(bp);change(s),fs83.mkdirSync(dbDir(bp),{recursive:!0,mode:448});let tmp=`${stateFile2(bp)}.${process.pid}.tmp`;return fs83.writeFileSync(tmp,JSON.stringify(s,null,2),{mode:384}),fs83.renameSync(tmp,stateFile2(bp)),s}function dbSecrets(bp){try{return loadSecrets(bp.secretsFile).database??{}}catch{return{}}}function changeSecrets(bp,change){let all=loadSecrets(bp.secretsFile),d={...all.database??{},stories:{...all.database?.stories??{}}};change(d),d.address||delete d.address,Object.keys(d.stories).length||delete d.stories;let{database:_old,...rest}=all;saveSecrets(bp.secretsFile,Object.keys(d).length?{...rest,database:d}:rest)}function keptAddress(bp,db){return dbSecrets(bp).address??db?.address}function personsAddress(bp,db=readMeta(bp).database){let a=keptAddress(bp,db);if(!a||db?.kind!=="sqlite")return a;let file3=readAddress2(a).name;if(!file3||path78.isAbsolute(file3))return a;let prisma=db.from?.find(f=>f.endsWith(".prisma"));return sqliteAt(a,file3,path78.join(bp.root,db.repo,prisma?path78.posix.dirname(prisma):".",file3))}function scrub2(bp,text3){let s=dbSecrets(bp),out18=text3;for(let a of[s.address,...Object.values(s.stories??{})]){if(!a)continue;let pass=/^(?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:([^@/]+)@/i.exec(a)?.[1]??/(?:^|;)\s*(?:password|pwd)\s*=([^;]+)/i.exec(a)?.[1];pass&&(out18=out18.split(pass).join("***").split(decodeURIComponent(pass)).join("***"))}return out18}function noteForEngineers(bp,text3,story2){let clean4=scrub2(bp,text3);if(readState(bp).notes.some(n=>n.story===story2&&n.text===clean4))return;changeState(bp,s=>s.notes=[...s.notes,{at:now(),...story2?{story:story2}:{},text:clean4}].slice(-30));let project;try{project=readMeta(bp).project}catch{}writeLog({level:"warn",...project?{project}:{},...story2?{ticket:story2}:{},text:clean4})}var sameSetting=(a,b)=>a.env===b.env&&a.file===b.file&&a.key===b.key,sameDatabase=(a,b)=>!!a&&a.kind===b.kind&&a.repo===b.repo&&a.name===b.name&&sameSetting(a.setting,b.setting);function learnDatabase(bp,opts={}){let meta=readMeta(bp),before=meta.database;if(before?.confirmed&&!opts.again)return{database:before,changed:!1};let look=findDatabase(bp.root,reposOf(meta).map(r=>r.path));if(!look.found)return look.later&&noteForEngineers(bp,look.later),{...before?{database:before}:{},changed:!1,...look.later?{later:look.later}:{}};let f=look.found,secret=f.address&&hasPassword(f.address)?f.address:void 0,next={kind:f.kind,repo:f.repo,...f.name?{name:f.name}:{},...f.where?{where:f.where}:{},setting:f.setting,...f.address&&!secret?{address:f.address}:{},...f.from.length?{from:f.from}:{},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return before?.confirmed&&sameDatabase(before,next)&&(next.confirmed=before.confirmed),writeMeta(bp,{...meta,database:next}),f.address&&changeSecrets(bp,d=>d.address=secret),{database:next,changed:!sameDatabase(before,next)}}function learnAfterRead(bp){try{learnDatabase(bp)}catch(err2){writeLog({level:"warn",text:scrub2(bp,`Learning the project's database did not work: ${err2.message}`)})}}var ENV_NAME=/^[A-Za-z_][A-Za-z0-9_]*$/;function confirmDatabase(bp,by2,change={}){let meta=readMeta(bp),repos=reposOf(meta).map(r=>r.path),kind=change.kind?.trim().toLowerCase();if(kind&&!DB_KINDS.includes(kind))throw new Error(`"${change.kind}" is not a kind squad-kit can copy. Use postgres, mysql, mongo or sqlite.`);let before=meta.database;if(!before&&!kind)throw new Error("squad-kit has not found the project's database. Name it: squad db confirm --kind postgres --env DATABASE_URL --address postgres://localhost:5432/app");let repo=change.repo?.trim()||before?.repo||(repos.length===1?repos[0]:"");if(!repos.includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repos.join(", ")}.`:`Say which repo reads the database: ${repos.join(", ")}.`);let setting={...before?.setting??{}};if(change.env!==void 0){let env2=change.env.trim();if(env2&&!ENV_NAME.test(env2))throw new Error(`"${env2}" is not a setting name. Write it as the app reads it, like DATABASE_URL.`);env2?setting.env=env2:delete setting.env}if(change.file!==void 0){let file3=change.file.trim().replace(/\\/g,"/");if(file3&&(file3.startsWith("/")||/^[a-z]:/i.test(file3)||file3.split("/").includes("..")))throw new Error("The settings file must be inside the repo, like .env or appsettings.Development.json.");file3?setting.file=file3:delete setting.file}if(change.key!==void 0){let key=change.key.trim();key?setting.key=key:delete setting.key}if(!setting.env&&!(setting.file&&setting.key))throw new Error("Name the setting that points the app at the database: --env DATABASE_URL, or --file appsettings.Development.json --key ConnectionStrings:Default.");let address=change.address?.trim(),finalKind=kind??before.kind;if(address){let said=kindOf(address);if(said&&said!==finalKind)throw new Error(`That address is for ${said==="sqlserver"?"SQL Server":KIND_NAME[said]}, not ${KIND_NAME[finalKind]}.`)}let name=change.name?.trim()||(address?readAddress2(address).name:void 0)||(kind&&kind!==before?.kind?void 0:before?.name),shared=address!==void 0?address&&!hasPassword(address)?address:void 0:before?.address,next={kind:finalKind,repo,...name?{name}:{},...before?.where&&finalKind===before.kind?{where:before.where}:{},setting,...shared?{address:shared}:{},...before?.from?{from:before.from}:{},confirmed:{by:by2,at:now()},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return writeMeta(bp,{...meta,database:next}),address!==void 0&&changeSecrets(bp,d=>d.address=address&&hasPassword(address)?address:void 0),next}function setDatabaseOptions(bp,o){let meta=readMeta(bp),db=meta.database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");let next={...db};if(o.hidePersonal===!0?next.hidePersonal=!0:o.hidePersonal===!1&&delete next.hidePersonal,o.sampleRows===null)delete next.sampleRows;else if(o.sampleRows!==void 0){let n=Math.round(Number(o.sampleRows));if(!Number.isFinite(n)||n<10||n>1e6)throw new Error("The sample size is a number of rows from 10 to 1,000,000, like 1000.");next.sampleRows=n}return writeMeta(bp,{...meta,database:next}),next}function templateInfo(bp){try{return JSON.parse(fs83.readFileSync(path78.join(templateFolder(bp),"template.json"),"utf8"))}catch{return null}}function currentTemplate(bp,source){try{return readTemplate2(templateFolder(bp),source)}catch{return null}}var making=new Map;function makeDbTemplate(bp,by2){let run6=making.get(bp.squadDir);if(run6)return run6;let next=makeOnce(bp,by2).finally(()=>making.delete(bp.squadDir));return making.set(bp.squadDir,next),next}function templateReady(bp){let db=readMeta(bp).database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");if(!db.confirmed)throw new Error("An engineer confirms the database first: squad db confirm");let source=personsAddress(bp,db);if(!source)throw new Error("squad-kit needs the database's address to make the template. Give it with: squad db confirm --address \u2026");let ignored=storyCopyIgnored(path78.join(bp.root,db.repo),db,source);if(ignored)throw new Error(ignored);let adapter=dbAdapter(db.kind),missing2=adapter.missingTools();if(missing2.length)throw new Error(`Making a template of a ${KIND_NAME[db.kind]} database needs ${missing2.join(" and ")} on this computer.`);return{db,source,adapter}}async function makeOnce(bp,by2){let{db,source,adapter}=templateReady(bp);changeState(bp,s=>s.making={since:now(),by:by2});try{let t=await adapter.makeTemplate(source,{folder:templateFolder(bp),hidePersonal:!!db.hidePersonal,...db.sampleRows?{sampleRows:db.sampleRows}:{}});return changeState(bp,s=>delete s.failed),writeLog({level:"info",project:readMeta(bp).project,text:`Made the template of the ${KIND_NAME[db.kind]} database ${t.info.sourceName}: ${t.info.tables.length} tables, ${t.info.bytes} bytes`}),t.info}catch(err2){let text3=scrub2(bp,err2.message);throw changeState(bp,s=>s.failed={at:now(),text:text3}),new Error(text3)}finally{changeState(bp,s=>delete s.making)}}function makeTemplateBeside(bp,by2){templateReady(bp),makeDbTemplate(bp,by2).catch(err2=>noteForEngineers(bp,`The template of the database was not made: ${err2.message}`))}function formOf(file3){let base2=path78.posix.basename(file3);return/^\.env(?:\.|$)/.test(base2)?"env":base2.endsWith(".properties")?"properties":base2.endsWith(".json")?"json":null}function copyFile(dir,setting){if(!setting.file)return;let form=formOf(setting.file),key=form==="env"?setting.key??setting.env:setting.key;if(!(!form||!key)&&git(dir,["check-ignore","--",setting.file],{allowFail:!0}))return{path:setting.file,key,form}}function wire(dir,db,address){let file3=copyFile(dir,db.setting);wireCopy(dir,{env:db.setting.env?{[db.setting.env]:address}:{},value:address,...file3?{file:file3}:{}})}var SQLITE_BESIDE=["","-wal","-shm","-journal"];function keepOutOfCommits(dir,db,address){if(db.kind!=="sqlite")return;let file3=readAddress2(address).name;file3&&markDbMade(dir,SQLITE_BESIDE.map(end=>`${path78.resolve(dir,file3)}${end}`))}function asBefore(bp,story2,why){return noteForEngineers(bp,why,story2),{state:"as-before",why:scrub2(bp,why)}}function personsOwn(adapter,address,source,dir,kind){if(address.trim()===source.trim())return!0;try{if(adapter.isPersonsOwn(address,source))return!0}catch{return!0}if(kind==="sqlite"){let file3=adapter.nameOf(address);return!path78.resolve(file3).startsWith(`${path78.resolve(dir)}${path78.sep}`)}return!1}function sqliteAt(address,from,to){return address.includes(from)?address.split(from).join(to):to}async function setUpStoryDb(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo)return{state:"none"};if(!db.confirmed&&db.kind==="sqlite")return{state:"none"};if(!db.confirmed)return asBefore(bp,story2,`squad-kit found the project's database, and no engineer has confirmed it yet, so ${story2} uses it as before. Confirm it on Harness, under Safety, or with: squad db confirm`);if(!db.setting.env&&!(db.setting.file&&db.setting.key))return asBefore(bp,story2,`squad-kit does not know which setting points the app at the database, so ${story2} uses it as before. Name it with: squad db confirm --env DATABASE_URL`);let source=personsAddress(bp,db);if(!source)return asBefore(bp,story2,`squad-kit does not know the database's address, so ${story2} uses it as before. Give it with: squad db confirm --address \u2026`);let adapter;try{adapter=dbAdapter(db.kind)}catch(err2){return asBefore(bp,story2,`${story2} uses the database as before: ${err2.message}`)}let ref2={id:story2,folder:dir},had=readState(bp).stories.find(s=>s.story===story2),kept=dbSecrets(bp).stories?.[story2];if(had&&kept&&(db.kind!=="sqlite"||path78.resolve(had.dir)===path78.resolve(dir)))return personsOwn(adapter,kept,source,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(path78.resolve(had.dir)!==path78.resolve(dir)&&(unwireCopy(had.dir),changeState(bp,s=>s.stories=s.stories.map(x=>x.story===story2?{...x,dir}:x))),wire(dir,db,kept),keepOutOfCommits(dir,db,kept),{state:"own",name:had.name,made:!1});let ignored=storyCopyIgnored(dir,db,source);if(ignored)return asBefore(bp,story2,`${story2} uses the database as before. ${ignored}`);let template=currentTemplate(bp,source);if(!template){if(db.kind!=="sqlite")return asBefore(bp,story2,`There is no template of the database yet, so ${story2} uses it as before. An engineer makes one on Harness, under Safety, or with: squad db template`);let raw=keptAddress(bp,db),file3=readAddress2(raw).name,own2=file3&&path78.isAbsolute(file3)?path78.relative(path78.join(bp.root,repo),file3):"";return own2&&!own2.startsWith("..")&&wire(dir,db,sqliteAt(raw,file3,path78.join(dir,own2))),{state:"none"}}let made;try{made=await adapter.createStoryDb(template,ref2)}catch(err2){let words2=err2 instanceof DbError&&err2.reason==="persons-own"?`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`:`${story2} uses the database as before: ${err2.message}`;return asBefore(bp,story2,words2)}return personsOwn(adapter,made.address,source,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(changeSecrets(bp,d=>d.stories={...d.stories,[story2]:made.address}),changeState(bp,s=>s.stories=[...s.stories.filter(x=>x.story!==story2),{story:story2,repo,dir,kind:db.kind,name:made.name,at:now()}]),wire(dir,db,made.address),keepOutOfCommits(dir,db,made.address),{state:"own",name:made.name,made:!made.existed})}function rewireStoryDbs(bp){let db;try{db=readMeta(bp).database}catch{return}if(db)for(let r of readState(bp).stories)fs83.existsSync(r.dir)&&rewireStoryDb(bp,r.story,r.dir,r.repo)}function rewireStoryDb(bp,story2,dir,repo){let db;try{db=readMeta(bp).database}catch{return!1}let had=readState(bp).stories.find(s=>s.story===story2&&s.repo===repo),kept=dbSecrets(bp).stories?.[story2];if(!db||!had||!kept||path78.resolve(had.dir)!==path78.resolve(dir))return!1;let source=personsAddress(bp,db);try{if(!source||personsOwn(dbAdapter(db.kind),kept,source,dir,db.kind))return!1}catch{return!1}return wire(dir,db,kept),keepOutOfCommits(dir,db,kept),!0}async function dropStoryDb(bp,story2){let had=readState(bp).stories.find(s=>s.story===story2);if(!had)return!1;let address=dbSecrets(bp).stories?.[story2],forget=()=>{let file3=had.kind==="sqlite"&&address?readAddress2(address).name:void 0;file3&&forgetDbMade(path78.join(bp.root,had.repo),SQLITE_BESIDE.map(end=>path78.relative(had.dir,path78.resolve(had.dir,file3))+end)),changeState(bp,s=>s.stories=s.stories.filter(x=>x.story!==story2)),changeSecrets(bp,d=>{d.stories&&delete d.stories[story2]}),unwireCopy(had.dir)},db=readMeta(bp).database,source=personsAddress(bp,db);if(!address)return forget(),!1;if(!source)return noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database: it does not know your database's address any more, to make sure it is not yours. Drop it yourself when you no longer need it.`,story2),!1;try{let gone=await dbAdapter(had.kind).dropStoryDb(address,source);return forget(),gone&&writeLog({level:"info",project:readMeta(bp).project,ticket:story2,text:`Dropped ${had.name}, the story's own database`}),gone}catch(err2){return err2 instanceof DbError&&(err2.reason==="not-ours"||err2.reason==="persons-own"||err2.reason==="bad-address")?(forget(),noteForEngineers(bp,`squad-kit left ${had.name} alone: ${err2.message}`,story2),!1):(noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database, yet. It tries again later: ${err2.message}`,story2),!1)}}function doneWith(bp,story2){let t;try{t=readTicket(bp,story2)}catch{return!1}return!t||!!t.mergedAt||t.stage==="done"||!!t.archived}async function tidyStoryDbs(bp){let out18=[];for(let r of readState(bp).stories)doneWith(bp,r.story)&&await dropStoryDb(bp,r.story)&&out18.push(r.story);return out18}var TIDY_MS=2*6e4,tidies=new Map;function tidyStoryDbsBeside(bp,at3=Date.now(),soon=!1){let t=tidies.get(bp.squadDir)??{at:0,busy:!1};if(tidies.set(bp.squadDir,t),t.busy||!soon&&at3<t.at)return;if(!readState(bp).stories.length){t.at=at3+TIDY_MS;return}t.busy=!0;let go=()=>{tidyStoryDbs(bp).catch(()=>{}).finally(()=>{t.busy=!1,t.at=Date.now()+TIDY_MS})};soon?setTimeout(go,0).unref():go()}function databaseView(bp){let db=readMeta(bp).database,s=readState(bp),address=personsAddress(bp,db),info2=templateInfo(bp),missing2=[];try{db&&(missing2=dbAdapter(db.kind).missingTools())}catch{missing2=[]}let live=s.stories.map(r=>({...r,...readTicket(bp,r.story)?.title?{title:readTicket(bp,r.story).title}:{}})),blocked=db?storyCopyIgnored(path78.join(bp.root,db.repo),db,address):void 0,ready=!!(db?.confirmed&&info2&&!blocked);return{database:db?{...db,...address?{address:withoutPassword(address)}:{},secret:!!dbSecrets(bp).address,kindName:KIND_NAME[db.kind]}:null,template:info2?{...info2,source:withoutPassword(info2.source)}:null,...s.making?{making:s.making}:{},...s.failed?{failed:s.failed}:{},...blocked?{blocked:scrub2(bp,blocked)}:{},stories:live,notes:[...s.notes].reverse().slice(0,10),missing:missing2,words:ready?"Each story gets its own copy of the database, made from a small template. Your own database is never changed.":blocked?"Stories use the database as before. The app chooses its database itself, so squad-kit cannot give each story a copy.":db?"Stories use the database as before, until an engineer sets up a copy for each story.":"squad-kit has not found a database in this project."}}var PullRequestClash=class extends Error{constructor(tickets,base2){super(`The pull request of ${tickets.join(", ")} clashes with newer changes in ${base2}.`);this.tickets=tickets;this.base=base2}tickets;base},PIPELINE_MS=20*6e4,ticketMessage=t=>`${t.title} (${t.id})`,ticketWords=t=>conventionalWords(t.kind==="bug"?"fix":"feat",t.title,t.id);async function commitTicket(ticket,how={}){if(!ticket.worktree||!fs84.existsSync(ticket.worktree))return null;await stageAllLater(ticket.worktree);let owed=ticket.hookOwed&&ticket.baseCommit;return!owed&&!somethingToSave(ticket.worktree)?null:(await commitChecked(ticket.worktree,ticketMessage(ticket),ticketWords(ticket),HOOK_MS,{...how,...owed?{from:ticket.baseCommit}:{}}),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function commitWithoutCheck(ticket){return!ticket.worktree||!fs84.existsSync(ticket.worktree)||(stageAll(ticket.worktree),!somethingToSave(ticket.worktree))?null:(commitWithoutHooks(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}async function commitWithoutCheckLater(ticket){return!ticket.worktree||!fs84.existsSync(ticket.worktree)||(await stageAllLater(ticket.worktree),!somethingToSave(ticket.worktree))?null:(await commitLater(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function hooksStillOwed(ticket){return ticket.hookOwed?!0:!ticket.worktree||!fs84.existsSync(ticket.worktree)?!1:(stageAll(ticket.worktree),bringHookRunner(ticket.worktree),somethingToSave(ticket.worktree)&&commitHooks(ticket.worktree).length>0)}async function commitRelease(dir,version,log3,how={}){let message=keepsCommitRules(dir)?conventionalWords("chore",`release ${version}`):`release ${version}`;try{await commitChecked(dir,message,conventionalWords("chore",`release ${version}`),SHORT_HOOK_MS,{...how,noRunner:!0})}catch(err2){if(!(err2 instanceof HookRefused))throw err2;await commitLater(dir,message),log3.push({ok:!1,text:`The project's own check said no to the release's own save, which holds only the version and the changelog, so squad-kit saved it without that check: ${err2.said.split(`
604
+ `).map(l=>/^(.+?):(\d+):(.*)$/.exec(l)).filter(m=>!!m).map(m=>({file:m[1],line:m[2],text:m[3]}))}var escapeRe=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function composeSets(dir,files,name){let passes=new RegExp(`^\\$(?:\\{${escapeRe(name)}(?::?-[^}]*)?\\}|${escapeRe(name)})$`);for(let file3 of files){let doc;try{doc=yaml12.load(read2(path77.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let env2=raw?.environment,value;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&s.slice(0,at3)===name&&(value=s.slice(at3+1))}else env2&&typeof env2=="object"&&(value=env2[name]);if(!(value==null||!String(value).trim())&&!passes.test(String(value).trim()))return{file:file3,service}}}}function storyCopyIgnored(dir,db,address){if(!SERVER_KINDS.includes(db.kind))return;let setting=db.setting.env??db.setting.key??"its setting",takes=`squad-kit gives each story its own copy when the app takes the database's name from ${setting}.`,name=db.name??(address?readAddress2(address).name:void 0);if(name&&!COMMON_NAMES.has(name.toLowerCase())){let quoted4=[`"${name}"`,`'${name}'`,`\`${name}\``],reads=text3=>text3.replace(/\bGetConnectionString\s*\(\s*(["'`])[^"'`]*\1\s*\)|\bConnectionStrings[:_]+\w+/g,""),hits=grepLines(dir,["-F",...quoted4.flatMap(q4=>["-e",q4])]).map(h=>({...h,text:reads(h.text)})).filter(h=>quoted4.some(q4=>h.text.includes(q4))&&DB_WORDS.test(quoted4.reduce((t,q4)=>t.split(q4).join(""),h.text))),at3=h=>`${h.file}:${h.line}`;if(hits.length)return`The code names the database ${name} itself, in ${hits.length===1?at3(hits[0]):`${hits.length} places like ${at3(hits[0])}`}, so it would not use a story's own copy under another name. ${takes}`}for(let h of grepLines(dir,["-E","-e",OPENS_GREP])){let other=TEST_PATH.test(h.file)?void 0:OPENS.exec(h.text)?.[2];if(other)return`The code opens the database ${other} by its name, in ${h.file}:${h.line}, so it would not use a story's own copy under another name. ${takes}`}let files=filesOf2(dir),values=[];if(db.setting.env)for(let f of files.env){let v=envPairs(read2(path77.join(dir,f))).get(db.setting.env);v&&values.push({value:v,file:f})}address&&values.push({value:address,file:""});for(let{value,file:file3}of values)if(!(!(kindOf(value)||/:\/\//.test(value))||readAddress2(value).name))return`The address in ${file3||setting} names no database, so the app picks its database some other way and would not use a story's own copy. ${takes}`;if(db.setting.env){let set2=composeSets(dir,files.compose,db.setting.env);if(set2)return`${set2.file} sets ${db.setting.env} itself for the ${set2.service} service, from other settings, so the app there would not see a story's own database.`}}var OWN_DB_WORDS="The story has its own copy of the database.",DB_KINDS=["postgres","mysql","mongo","sqlite"],KIND_NAME={postgres:"Postgres",mysql:"MySQL",mongo:"Mongo",sqlite:"SQLite"},dbDir=bp=>path78.join(bp.localDir,"database"),templateFolder=bp=>path78.join(dbDir(bp),"template"),stateFile2=bp=>path78.join(dbDir(bp),"state.json");function readState(bp){try{let s=JSON.parse(fs83.readFileSync(stateFile2(bp),"utf8"));return{...s,stories:Array.isArray(s.stories)?s.stories:[],notes:Array.isArray(s.notes)?s.notes:[]}}catch{return{stories:[],notes:[]}}}function changeState(bp,change){let s=readState(bp);change(s),fs83.mkdirSync(dbDir(bp),{recursive:!0,mode:448});let tmp=`${stateFile2(bp)}.${process.pid}.tmp`;return fs83.writeFileSync(tmp,JSON.stringify(s,null,2),{mode:384}),fs83.renameSync(tmp,stateFile2(bp)),s}function dbSecrets(bp){try{return loadSecrets(bp.secretsFile).database??{}}catch{return{}}}function changeSecrets(bp,change){let all=loadSecrets(bp.secretsFile),d={...all.database??{},stories:{...all.database?.stories??{}}};change(d),d.address||delete d.address,Object.keys(d.stories).length||delete d.stories;let{database:_old,...rest}=all;saveSecrets(bp.secretsFile,Object.keys(d).length?{...rest,database:d}:rest)}function keptAddress(bp,db){return dbSecrets(bp).address??db?.address}function personsAddress(bp,db=readMeta(bp).database){let a=keptAddress(bp,db);if(!a||db?.kind!=="sqlite")return a;let file3=readAddress2(a).name;if(!file3||path78.isAbsolute(file3))return a;let prisma=db.from?.find(f=>f.endsWith(".prisma"));return sqliteAt(a,file3,path78.join(bp.root,db.repo,prisma?path78.posix.dirname(prisma):".",file3))}function scrub2(bp,text3){let s=dbSecrets(bp),out18=text3;for(let a of[s.address,...Object.values(s.stories??{})]){if(!a)continue;let pass=/^(?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:([^@/]+)@/i.exec(a)?.[1]??/(?:^|;)\s*(?:password|pwd)\s*=([^;]+)/i.exec(a)?.[1];pass&&(out18=out18.split(pass).join("***").split(decodeURIComponent(pass)).join("***"))}return out18}function noteForEngineers(bp,text3,story2){let clean4=scrub2(bp,text3);if(readState(bp).notes.some(n=>n.story===story2&&n.text===clean4))return;changeState(bp,s=>s.notes=[...s.notes,{at:now(),...story2?{story:story2}:{},text:clean4}].slice(-30));let project;try{project=readMeta(bp).project}catch{}writeLog({level:"warn",...project?{project}:{},...story2?{ticket:story2}:{},text:clean4})}var sameSetting=(a,b)=>a.env===b.env&&a.file===b.file&&a.key===b.key,sameDatabase=(a,b)=>!!a&&a.kind===b.kind&&a.repo===b.repo&&a.name===b.name&&sameSetting(a.setting,b.setting);function learnDatabase(bp,opts={}){let meta=readMeta(bp),before=meta.database;if(before?.confirmed&&!opts.again)return{database:before,changed:!1};let look=findDatabase(bp.root,reposOf(meta).map(r=>r.path));if(!look.found)return look.later&&noteForEngineers(bp,look.later),{...before?{database:before}:{},changed:!1,...look.later?{later:look.later}:{}};let f=look.found,secret=f.address&&hasPassword(f.address)?f.address:void 0,next={kind:f.kind,repo:f.repo,...f.name?{name:f.name}:{},...f.where?{where:f.where}:{},setting:f.setting,...f.address&&!secret?{address:f.address}:{},...f.from.length?{from:f.from}:{},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return before?.confirmed&&sameDatabase(before,next)&&(next.confirmed=before.confirmed),writeMeta(bp,{...meta,database:next}),f.address&&changeSecrets(bp,d=>d.address=secret),{database:next,changed:!sameDatabase(before,next)}}function learnAfterRead(bp){try{learnDatabase(bp)}catch(err2){writeLog({level:"warn",text:scrub2(bp,`Learning the project's database did not work: ${err2.message}`)})}}var ENV_NAME=/^[A-Za-z_][A-Za-z0-9_]*$/;function confirmDatabase(bp,by2,change={}){let meta=readMeta(bp),repos=reposOf(meta).map(r=>r.path),kind=change.kind?.trim().toLowerCase();if(kind&&!DB_KINDS.includes(kind))throw new Error(`"${change.kind}" is not a kind squad-kit can copy. Use postgres, mysql, mongo or sqlite.`);let before=meta.database;if(!before&&!kind)throw new Error("squad-kit has not found the project's database. Name it: squad db confirm --kind postgres --env DATABASE_URL --address postgres://localhost:5432/app");let repo=change.repo?.trim()||before?.repo||(repos.length===1?repos[0]:"");if(!repos.includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repos.join(", ")}.`:`Say which repo reads the database: ${repos.join(", ")}.`);let setting={...before?.setting??{}};if(change.env!==void 0){let env2=change.env.trim();if(env2&&!ENV_NAME.test(env2))throw new Error(`"${env2}" is not a setting name. Write it as the app reads it, like DATABASE_URL.`);env2?setting.env=env2:delete setting.env}if(change.file!==void 0){let file3=change.file.trim().replace(/\\/g,"/");if(file3&&(file3.startsWith("/")||/^[a-z]:/i.test(file3)||file3.split("/").includes("..")))throw new Error("The settings file must be inside the repo, like .env or appsettings.Development.json.");file3?setting.file=file3:delete setting.file}if(change.key!==void 0){let key=change.key.trim();key?setting.key=key:delete setting.key}if(!setting.env&&!(setting.file&&setting.key))throw new Error("Name the setting that points the app at the database: --env DATABASE_URL, or --file appsettings.Development.json --key ConnectionStrings:Default.");let address=change.address?.trim(),finalKind=kind??before.kind;if(address){let said=kindOf(address);if(said&&said!==finalKind)throw new Error(`That address is for ${said==="sqlserver"?"SQL Server":KIND_NAME[said]}, not ${KIND_NAME[finalKind]}.`)}let name=change.name?.trim()||(address?readAddress2(address).name:void 0)||(kind&&kind!==before?.kind?void 0:before?.name),shared=address!==void 0?address&&!hasPassword(address)?address:void 0:before?.address,next={kind:finalKind,repo,...name?{name}:{},...before?.where&&finalKind===before.kind?{where:before.where}:{},setting,...shared?{address:shared}:{},...before?.from?{from:before.from}:{},confirmed:{by:by2,at:now()},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return writeMeta(bp,{...meta,database:next}),address!==void 0&&changeSecrets(bp,d=>d.address=address&&hasPassword(address)?address:void 0),next}function setDatabaseOptions(bp,o){let meta=readMeta(bp),db=meta.database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");let next={...db};if(o.hidePersonal===!0?next.hidePersonal=!0:o.hidePersonal===!1&&delete next.hidePersonal,o.sampleRows===null)delete next.sampleRows;else if(o.sampleRows!==void 0){let n=Math.round(Number(o.sampleRows));if(!Number.isFinite(n)||n<10||n>1e6)throw new Error("The sample size is a number of rows from 10 to 1,000,000, like 1000.");next.sampleRows=n}return writeMeta(bp,{...meta,database:next}),next}function templateInfo(bp){try{return JSON.parse(fs83.readFileSync(path78.join(templateFolder(bp),"template.json"),"utf8"))}catch{return null}}function currentTemplate(bp,source){try{return readTemplate2(templateFolder(bp),source)}catch{return null}}var making=new Map;function makeDbTemplate(bp,by2){let run6=making.get(bp.squadDir);if(run6)return run6;let next=makeOnce(bp,by2).finally(()=>making.delete(bp.squadDir));return making.set(bp.squadDir,next),next}function templateReady(bp){let db=readMeta(bp).database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");if(!db.confirmed)throw new Error("An engineer confirms the database first: squad db confirm");let source=personsAddress(bp,db);if(!source)throw new Error("squad-kit needs the database's address to make the template. Give it with: squad db confirm --address \u2026");let ignored=storyCopyIgnored(path78.join(bp.root,db.repo),db,source);if(ignored)throw new Error(ignored);let adapter=dbAdapter(db.kind),missing2=adapter.missingTools();if(missing2.length)throw new Error(`Making a template of a ${KIND_NAME[db.kind]} database needs ${missing2.join(" and ")} on this computer.`);return{db,source,adapter}}async function makeOnce(bp,by2){let{db,source,adapter}=templateReady(bp);changeState(bp,s=>s.making={since:now(),by:by2});try{let t=await adapter.makeTemplate(source,{folder:templateFolder(bp),hidePersonal:!!db.hidePersonal,...db.sampleRows?{sampleRows:db.sampleRows}:{}});return changeState(bp,s=>delete s.failed),writeLog({level:"info",project:readMeta(bp).project,text:`Made the template of the ${KIND_NAME[db.kind]} database ${t.info.sourceName}: ${t.info.tables.length} tables, ${t.info.bytes} bytes`}),t.info}catch(err2){let text3=scrub2(bp,err2.message);throw changeState(bp,s=>s.failed={at:now(),text:text3}),new Error(text3)}finally{changeState(bp,s=>delete s.making)}}function makeTemplateBeside(bp,by2){templateReady(bp),makeDbTemplate(bp,by2).catch(err2=>noteForEngineers(bp,`The template of the database was not made: ${err2.message}`))}function formOf(file3){let base2=path78.posix.basename(file3);return/^\.env(?:\.|$)/.test(base2)?"env":base2.endsWith(".properties")?"properties":base2.endsWith(".json")?"json":null}function copyFile(dir,setting){if(!setting.file)return;let form=formOf(setting.file),key=form==="env"?setting.key??setting.env:setting.key;if(!(!form||!key)&&git(dir,["check-ignore","--",setting.file],{allowFail:!0}))return{path:setting.file,key,form}}function wire(dir,db,address){let file3=copyFile(dir,db.setting);wireCopy(dir,{env:db.setting.env?{[db.setting.env]:address}:{},value:address,...file3?{file:file3}:{}})}var SQLITE_BESIDE=["","-wal","-shm","-journal"];function keepOutOfCommits(dir,db,address){if(db.kind!=="sqlite")return;let file3=readAddress2(address).name;file3&&markDbMade(dir,SQLITE_BESIDE.map(end=>`${path78.resolve(dir,file3)}${end}`))}function asBefore(bp,story2,why){return noteForEngineers(bp,why,story2),{state:"as-before",why:scrub2(bp,why)}}function personsOwn(adapter,address,source,dir,kind){if(address.trim()===source.trim())return!0;try{if(adapter.isPersonsOwn(address,source))return!0}catch{return!0}if(kind==="sqlite"){let file3=adapter.nameOf(address);return!path78.resolve(file3).startsWith(`${path78.resolve(dir)}${path78.sep}`)}return!1}function sqliteAt(address,from,to){return address.includes(from)?address.split(from).join(to):to}async function setUpStoryDb(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo)return{state:"none"};if(!db.confirmed&&db.kind==="sqlite")return{state:"none"};if(!db.confirmed)return asBefore(bp,story2,`squad-kit found the project's database, and no engineer has confirmed it yet, so ${story2} uses it as before. Confirm it on Harness, under Safety, or with: squad db confirm`);if(!db.setting.env&&!(db.setting.file&&db.setting.key))return asBefore(bp,story2,`squad-kit does not know which setting points the app at the database, so ${story2} uses it as before. Name it with: squad db confirm --env DATABASE_URL`);let source=personsAddress(bp,db);if(!source)return asBefore(bp,story2,`squad-kit does not know the database's address, so ${story2} uses it as before. Give it with: squad db confirm --address \u2026`);let adapter;try{adapter=dbAdapter(db.kind)}catch(err2){return asBefore(bp,story2,`${story2} uses the database as before: ${err2.message}`)}let ref2={id:story2,folder:dir},had=readState(bp).stories.find(s=>s.story===story2),kept=dbSecrets(bp).stories?.[story2];if(had&&kept&&(db.kind!=="sqlite"||path78.resolve(had.dir)===path78.resolve(dir)))return personsOwn(adapter,kept,source,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(path78.resolve(had.dir)!==path78.resolve(dir)&&(unwireCopy(had.dir),changeState(bp,s=>s.stories=s.stories.map(x=>x.story===story2?{...x,dir}:x))),wire(dir,db,kept),keepOutOfCommits(dir,db,kept),{state:"own",name:had.name,made:!1});let ignored=storyCopyIgnored(dir,db,source);if(ignored)return asBefore(bp,story2,`${story2} uses the database as before. ${ignored}`);let template=currentTemplate(bp,source);if(!template){if(db.kind!=="sqlite")return asBefore(bp,story2,`There is no template of the database yet, so ${story2} uses it as before. An engineer makes one on Harness, under Safety, or with: squad db template`);let raw=keptAddress(bp,db),file3=readAddress2(raw).name,own2=file3&&path78.isAbsolute(file3)?path78.relative(path78.join(bp.root,repo),file3):"";return own2&&!own2.startsWith("..")&&wire(dir,db,sqliteAt(raw,file3,path78.join(dir,own2))),{state:"none"}}let made;try{made=await adapter.createStoryDb(template,ref2)}catch(err2){let words2=err2 instanceof DbError&&err2.reason==="persons-own"?`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`:`${story2} uses the database as before: ${err2.message}`;return asBefore(bp,story2,words2)}return personsOwn(adapter,made.address,source,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(changeSecrets(bp,d=>d.stories={...d.stories,[story2]:made.address}),changeState(bp,s=>s.stories=[...s.stories.filter(x=>x.story!==story2),{story:story2,repo,dir,kind:db.kind,name:made.name,at:now()}]),wire(dir,db,made.address),keepOutOfCommits(dir,db,made.address),{state:"own",name:made.name,made:!made.existed})}function rewireStoryDbs(bp){let db;try{db=readMeta(bp).database}catch{return}if(db)for(let r of readState(bp).stories)fs83.existsSync(r.dir)&&rewireStoryDb(bp,r.story,r.dir,r.repo)}function rewireStoryDb(bp,story2,dir,repo){let db;try{db=readMeta(bp).database}catch{return!1}let had=readState(bp).stories.find(s=>s.story===story2&&s.repo===repo),kept=dbSecrets(bp).stories?.[story2];if(!db||!had||!kept||path78.resolve(had.dir)!==path78.resolve(dir))return!1;let source=personsAddress(bp,db);try{if(!source||personsOwn(dbAdapter(db.kind),kept,source,dir,db.kind))return!1}catch{return!1}return wire(dir,db,kept),keepOutOfCommits(dir,db,kept),!0}async function remakeStoryDb(bp,story2,dir,repo){return!readState(bp).stories.find(s=>s.story===story2&&s.repo===repo&&path78.resolve(s.dir)===path78.resolve(dir))||!await dropStoryDb(bp,story2)?!1:(await setUpStoryDb(bp,story2,dir,repo)).state==="own"}function setAsideCopyDb(dir){let found2=findDatabase(dir,["."]).found,name=found2?.kind==="sqlite"&&found2.address?readAddress2(found2.address).name:void 0;if(!name)return[];let schemas=git(dir,["ls-files","--","*schema.prisma"],{allowFail:!0}).split(`
605
+ `).filter(Boolean),files=path78.isAbsolute(name)?[name]:[dir,...schemas.map(s=>path78.dirname(path78.join(dir,s)))].map(b=>path78.resolve(b,name)),inside5=`${path78.resolve(dir)}${path78.sep}`,moved=[];for(let file3 of new Set(files))if(!(!file3.startsWith(inside5)||!fs83.existsSync(file3)||git(dir,["ls-files","--",path78.relative(dir,file3)],{allowFail:!0})))for(let end of SQLITE_BESIDE)fs83.existsSync(`${file3}${end}`)&&(fs83.renameSync(`${file3}${end}`,`${file3}.out-of-step${end}`),moved.push(`${file3}.out-of-step${end}`));return moved.length&&markDbMade(dir,moved),moved}async function dropStoryDb(bp,story2){let had=readState(bp).stories.find(s=>s.story===story2);if(!had)return!1;let address=dbSecrets(bp).stories?.[story2],forget=()=>{let file3=had.kind==="sqlite"&&address?readAddress2(address).name:void 0;file3&&forgetDbMade(path78.join(bp.root,had.repo),SQLITE_BESIDE.map(end=>path78.relative(had.dir,path78.resolve(had.dir,file3))+end)),changeState(bp,s=>s.stories=s.stories.filter(x=>x.story!==story2)),changeSecrets(bp,d=>{d.stories&&delete d.stories[story2]}),unwireCopy(had.dir)},db=readMeta(bp).database,source=personsAddress(bp,db);if(!address)return forget(),!1;if(!source)return noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database: it does not know your database's address any more, to make sure it is not yours. Drop it yourself when you no longer need it.`,story2),!1;try{let gone=await dbAdapter(had.kind).dropStoryDb(address,source);return forget(),gone&&writeLog({level:"info",project:readMeta(bp).project,ticket:story2,text:`Dropped ${had.name}, the story's own database`}),gone}catch(err2){return err2 instanceof DbError&&(err2.reason==="not-ours"||err2.reason==="persons-own"||err2.reason==="bad-address")?(forget(),noteForEngineers(bp,`squad-kit left ${had.name} alone: ${err2.message}`,story2),!1):(noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database, yet. It tries again later: ${err2.message}`,story2),!1)}}function doneWith(bp,story2){let t;try{t=readTicket(bp,story2)}catch{return!1}return!t||!!t.mergedAt||t.stage==="done"||!!t.archived}async function tidyStoryDbs(bp){let out18=[];for(let r of readState(bp).stories)doneWith(bp,r.story)&&await dropStoryDb(bp,r.story)&&out18.push(r.story);return out18}var TIDY_MS=2*6e4,tidies=new Map;function tidyStoryDbsBeside(bp,at3=Date.now(),soon=!1){let t=tidies.get(bp.squadDir)??{at:0,busy:!1};if(tidies.set(bp.squadDir,t),t.busy||!soon&&at3<t.at)return;if(!readState(bp).stories.length){t.at=at3+TIDY_MS;return}t.busy=!0;let go=()=>{tidyStoryDbs(bp).catch(()=>{}).finally(()=>{t.busy=!1,t.at=Date.now()+TIDY_MS})};soon?setTimeout(go,0).unref():go()}function databaseView(bp){let db=readMeta(bp).database,s=readState(bp),address=personsAddress(bp,db),info2=templateInfo(bp),missing2=[];try{db&&(missing2=dbAdapter(db.kind).missingTools())}catch{missing2=[]}let live=s.stories.map(r=>({...r,...readTicket(bp,r.story)?.title?{title:readTicket(bp,r.story).title}:{}})),blocked=db?storyCopyIgnored(path78.join(bp.root,db.repo),db,address):void 0,ready=!!(db?.confirmed&&info2&&!blocked);return{database:db?{...db,...address?{address:withoutPassword(address)}:{},secret:!!dbSecrets(bp).address,kindName:KIND_NAME[db.kind]}:null,template:info2?{...info2,source:withoutPassword(info2.source)}:null,...s.making?{making:s.making}:{},...s.failed?{failed:s.failed}:{},...blocked?{blocked:scrub2(bp,blocked)}:{},stories:live,notes:[...s.notes].reverse().slice(0,10),missing:missing2,words:ready?"Each story gets its own copy of the database, made from a small template. Your own database is never changed.":blocked?"Stories use the database as before. The app chooses its database itself, so squad-kit cannot give each story a copy.":db?"Stories use the database as before, until an engineer sets up a copy for each story.":"squad-kit has not found a database in this project."}}var PullRequestClash=class extends Error{constructor(tickets,base2){super(`The pull request of ${tickets.join(", ")} clashes with newer changes in ${base2}.`);this.tickets=tickets;this.base=base2}tickets;base},PIPELINE_MS=20*6e4,ticketMessage=t=>`${t.title} (${t.id})`,ticketWords=t=>conventionalWords(t.kind==="bug"?"fix":"feat",t.title,t.id);async function commitTicket(ticket,how={}){if(!ticket.worktree||!fs84.existsSync(ticket.worktree))return null;await stageAllLater(ticket.worktree);let owed=ticket.hookOwed&&ticket.baseCommit;return!owed&&!somethingToSave(ticket.worktree)?null:(await commitChecked(ticket.worktree,ticketMessage(ticket),ticketWords(ticket),HOOK_MS,{...how,...owed?{from:ticket.baseCommit}:{}}),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function commitWithoutCheck(ticket){return!ticket.worktree||!fs84.existsSync(ticket.worktree)||(stageAll(ticket.worktree),!somethingToSave(ticket.worktree))?null:(commitWithoutHooks(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}async function commitWithoutCheckLater(ticket){return!ticket.worktree||!fs84.existsSync(ticket.worktree)||(await stageAllLater(ticket.worktree),!somethingToSave(ticket.worktree))?null:(await commitLater(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function hooksStillOwed(ticket){return ticket.hookOwed?!0:!ticket.worktree||!fs84.existsSync(ticket.worktree)?!1:(stageAll(ticket.worktree),bringHookRunner(ticket.worktree),somethingToSave(ticket.worktree)&&commitHooks(ticket.worktree).length>0)}async function commitRelease(dir,version,log3,how={}){let message=keepsCommitRules(dir)?conventionalWords("chore",`release ${version}`):`release ${version}`;try{await commitChecked(dir,message,conventionalWords("chore",`release ${version}`),SHORT_HOOK_MS,{...how,noRunner:!0})}catch(err2){if(!(err2 instanceof HookRefused))throw err2;await commitLater(dir,message),log3.push({ok:!1,text:`The project's own check said no to the release's own save, which holds only the version and the changelog, so squad-kit saved it without that check: ${err2.said.split(`
605
606
  `).slice(-1)[0]??""}`.trim()})}}function basesNow(bp){let meta=readMeta(bp);return meta.engine!=="live"?new Map:new Map(reposOf(meta).map(r=>{let root=path79.join(projectRoot(bp),r.path);return[r.path,git(root,["rev-parse","--verify","-q",`refs/heads/${r.base||currentBranch(root)}`],{allowFail:!0})]}))}async function liveRelease(bp,release,tickets,from=new Map){let root=projectRoot(bp),meta=readMeta(bp),groups=new Map;for(let t of tickets)for(let view of copyViews(meta,t)){let repo=repoFor(meta,view),g=groups.get(repo.path)??{repo,tickets:[]};g.tickets.push(view),groups.set(repo.path,g)}let log3=[],unpushed=[],multi=groups.size>1||!!meta.repos?.length,leaving=(release.out??[]).filter(id2=>!release.tickets.includes(id2)).map(id2=>readTicket(bp,id2)).filter(t=>!!(t?.mergedAt&&t.stage!=="done")).flatMap(t=>copyViews(meta,t)),order=repoOrder(meta),place=repo=>order.includes(repo)?order.indexOf(repo):order.length,plans=[...groups.values()].sort((a,b)=>place(a.repo.path)-place(b.repo.path)).map(({repo,tickets:list2})=>{let repoRoot=path79.join(root,repo.path),remote=repo.remote||remotes(repoRoot)[0],url=remote?git(repoRoot,["config","--get",`remote.${remote}.url`],{allowFail:!0}):"",host=url?repoHostFor(bp,url):null;return{repo,repoRoot,list:list2,host,env:host?.gitEnv()??{},leave:leaving.filter(t=>repoFor(meta,t).path===repo.path)}});for(let{repo,repoRoot,list:list2,host,leave}of plans){let clashing=[];for(let t of list2.filter(x=>host&&x.pr?.host===host.host&&!x.mergedAt))await host.mergeState(t.pr.id)==="clash"&&clashing.push(t.id);let base2=repo.base||currentBranch(repoRoot);for(let t of list2.filter(x=>x.branch&&!x.mergedAt&&!x.pr))mergeWouldClash(repoRoot,base2,t.branch)&&clashing.push(t.id);if(clashing.length)throw new PullRequestClash(clashing,repo.base||currentBranch(repoRoot));let problem2=leave.length?await leaveOutProblem(repoRoot,repo.base||currentBranch(repoRoot),leave):null;if(problem2)throw new Error(`${problem2} Put it back into ${release.version}, or take that work out too.`)}startJournal(bp,release.version,tickets.map(t=>t.id));let named=r=>repoLabel(meta,r)??r;for(let{repo,repoRoot,list:list2,host,env:env2,leave}of plans){let waitsFor=unpushed.find(u=>repoReliesOn(meta,repo.path,u.repo))?.repo,docs=docsInGit(bp)&&path79.resolve(docsRepo(bp))===path79.resolve(repoRoot)?dir=>stageReleaseDocs(bp,dir,release,list2,repo.base||currentBranch(repoRoot)):void 0,keep=path79.relative(repoRoot,bp.squadDir),note2=patch=>noteRepo(bp,repo.path,patch),done=await releaseRepo(repoRoot,repo,release,list2,meta.commands?.deploy,host,env2,docs,keep&&!keep.startsWith("..")?keep:void 0,{leave,note:note2,...from.get(repo.path)?{startAt:from.get(repo.path)}:{},...waitsFor?{holdPush:{plain:`${named(repo.path)} did not go out yet: it goes after ${named(waitsFor)}.`}}:{},merged:id2=>notePrMerged(bp,id2),guardFor:dir=>guardSupport().on?projectGuardFor(bp,{cwd:dir,...meta.guard?{allow:meta.guard}:{}}):void 0});log3.push(...done.log.map(l=>multi?{...l,text:`${repo.path}: ${l.text}`}:l)),done.unpushed&&unpushed.push(done.unpushed);for(let t of list2)dropTicketCache(bp,t.id)}for(let t of tickets)t.storyDir&&!copyDirsIn(t.storyDir).length&&fs84.rmSync(t.storyDir,{recursive:!0,force:!0});for(let t of tickets)await dropStoryDb(bp,t.id).catch(()=>!1);return{log:log3,unpushed}}async function releaseRepo(root,repo,release,tickets,deployFallback,host,env2,docs,keep,extra={}){let base2=repo.base||currentBranch(root),remote=repo.remote||remotes(root)[0],version=release.version,branch=`release/${version}`,dir=path79.join(worktreeRoot(root),`release-${version}`),log3=[],unpushed;for(let t of tickets.filter(x=>x.mergedAt))log3.push({ok:!0,text:`Already in the code: ${t.id} ${t.title}`});let viaPr=tickets.filter(t=>host&&t.pr?.host===host.host&&!t.mergedAt),note2=extra.note??(()=>{});for(let t of viaPr)await host.merge(t.pr.id,`Merge ${t.title} (${t.id})`),extra.merged?.(t.id),log3.push({ok:!0,text:`Merged pull request ${t.pr.id}: ${t.title}`});let start=extra.startAt??base2;(viaPr.length||tickets.some(t=>t.pr&&t.mergedAt))&&remote&&(start=await fetchedTip(root,remote,base2,{env:env2})),await removeWorktree(root,dir),await gitLater(root,["worktree","prune"],{allowFail:!0}),git(root,["branch","-D",branch],{allowFail:!0}),await copyTurn(dir,()=>makeCopy(root,dir,["-b",branch],start)),note2({base:base2,...remote?{remote}:{},dir,branch});try{for(let t of tickets)if(!(!t.branch||t.mergedAt||viaPr.includes(t)))try{await gitLater(dir,[...commitArgsFor(dir),"merge","--no-ff","-m",`Merge ${t.title} (${t.id})`,t.branch]),log3.push({ok:!0,text:`Merged ${t.id} ${t.title}`})}catch(err2){let clashes=await gitLater(dir,["diff","--name-only","--diff-filter=U"],{allowFail:!0});throw await gitLater(dir,["merge","--abort"],{allowFail:!0}),clashes?new PullRequestClash([t.id],base2):(writeLog({level:"error",ticket:t.id,text:`Merging into ${base2} for ${release.version}: ${err2.message}`}),new Error(`${t.id} could not be merged into ${base2}, so the release stopped. An engineer can see why in the log.`))}let reverts=extra.leave?.length?await leaveOut(dir,root,base2,extra.leave,version,log3):[];release.addons.changelog&&(writeChangelog(dir,version,release.notes),log3.push({ok:!0,text:"Changelog updated"}));let pkg=path79.join(dir,"package.json");if(fs84.existsSync(pkg)){let json2=JSON.parse(fs84.readFileSync(pkg,"utf8"));json2.version&&(json2.version=version.replace(/^v/,""),fs84.writeFileSync(pkg,`${JSON.stringify(json2,null,2)}
606
607
  `,"utf8"),log3.push({ok:!0,text:`Set the version to ${json2.version}`}))}docs&&remote&&await netGitLater(dir,["fetch","--quiet",remote,base2],{env:env2,allowFail:!0});for(let text3 of docs?.(dir)??[])log3.push({ok:!0,text:text3});await stageAllLater(dir),somethingToSave(dir)&&await commitRelease(dir,version,log3,extra.guardFor?.(dir)?{guard:extra.guardFor(dir)}:{}),note2({base:base2,commit:git(dir,["rev-parse","HEAD"]),...release.addons.tag?{tag:version}:{}}),release.addons.tag&&(git(dir,[...commitArgsFor(dir),"tag","-f","-a",version,"-m",`Release ${version}`]),log3.push({ok:!0,text:`Marked the code as ${version}`})),reverts.length&&await bringBack(dir,reverts,version,base2);let head=git(dir,["rev-parse","HEAD"]);note2({base:base2,head});let sent=head;if(remote&&extra.holdPush)unpushed={repo:repo.path,remote,base:base2,commit:head,...release.addons.tag?{tag:version}:{},error:extra.holdPush.plain,plain:extra.holdPush.plain},log3.push({ok:!1,text:extra.holdPush.plain});else if(remote)try{sent=await pushRelease(dir,remote,base2,version,release.notes,host,env2,log3),note2({base:base2,head:sent,pushed:!0}),release.addons.tag&&await netGitLater(dir,["push",remote,`refs/tags/${version}`],{env:env2}),log3.push({ok:!0,text:"Sent to your team's shared copy of the code"})}catch(err2){let error2=err2.message,plain=pushWords(error2);unpushed={repo:repo.path,remote,base:base2,commit:head,...release.addons.tag?{tag:version}:{},error:pushDetail(error2,{remote,base:base2,version}),plain},log3.push({ok:!1,text:`${plain} Press Send again, or run: squad release ${version} --push-again`})}else log3.push({ok:!0,text:"Saved on this computer only: this project has no online copy to send it to."});if(await moveLocalBase(root,base2,sent,version,keep,log3),note2({base:base2,moved:!0}),release.addons.pipeline){let deploy=repo.commands.deploy?.trim()||deployFallback?.trim();if(deploy)try{sent!==head&&await gitLater(dir,["checkout","--quiet","--detach",sent]);let r=await runCommand(dir,deploy,PIPELINE_MS,extra.guardFor?.(dir)?{guard:extra.guardFor(dir)}:{});r.ok||writeLog({level:"error",text:`The after-release step of ${version} failed (${deploy}): ${tail(r.output,3)}`}),log3.push({ok:r.ok,text:r.ok?"Ran your after-release step":"Your after-release step failed. An engineer can see why in the log."})}catch(err2){writeLog({level:"error",text:`The after-release step of ${version} did not run: ${err2.message}`}),log3.push({ok:!1,text:"Your after-release step did not run. An engineer can see why in the log."})}else log3.push({ok:!1,text:"No after-release step is set up in Settings, so none ran"})}}finally{await removeWorktree(root,dir)}let tip=git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0});!unpushed&&tip&&isAncestor(root,tip,base2)&&git(root,["branch","-D",branch],{allowFail:!0});for(let t of tickets)t.worktree&&await removeWorktree(root,t.worktree);return{log:log3,...unpushed?{unpushed}:{}}}function moveLocalBase(root,base2,head,version,keep,log3){return baseTurn(root,async()=>{let dirty=currentBranch(root)===base2?await gitLater(root,["status","--porcelain","--untracked-files=no","--",".",...keep?[`:(exclude)${keep}`]:[]],{allowFail:!0,env:{GIT_OPTIONAL_LOCKS:"0"}}):"";moveBaseNow(root,base2,head,version,keep,log3,dirty)})}function moveBaseNow(root,base2,head,version,keep,log3,dirty,again2=!1){let branch=`release/${version}`,before=git(root,["rev-parse",base2]);if(!isAncestor(root,before,head)){try{let r=mergeIntoBase(root,base2,head,`Merge release ${version} into ${base2}`,keep);r.note&&writeLog({level:"warn",text:`Moving ${base2} for ${version}: ${r.note}`}),log3.push(r.moved?{ok:!0,text:"The code here has work accepted while the release went out, so the release was merged into it"}:{ok:!1,text:`Your own copy of the code was not updated. An engineer can bring it in with: git merge ${branch}`})}catch(err2){err2 instanceof MergeClash||writeLog({level:"error",text:`Moving ${base2} for ${version}: ${err2.message}`});let why=err2 instanceof MergeClash?"it has work accepted while the release went out, and the two clash":"something technical stopped it";log3.push({ok:!1,text:`The code on this computer was not updated: ${why}. An engineer can bring it in with: git merge ${branch}`})}return}if(currentBranch(root)===base2){if(dirty)log3.push({ok:!1,text:`Your own copy of the code has changes, so it was not updated. An engineer can bring it in with: git merge ${branch}`});else try{keepingFiles(root,keep,before,head,()=>git(root,["merge","--ff-only",head])),log3.push({ok:!0,text:"Your own copy of the code now has the release"})}catch(err2){writeLog({level:"warn",text:`Moving ${base2} for ${version}: ${err2.message}`}),log3.push({ok:!1,text:`Your own copy of the code was not updated. An engineer can bring it in with: git merge --ff-only ${head.slice(0,8)}`})}return}try{git(root,["update-ref",`refs/heads/${base2}`,head,before])}catch(err2){if(!again2)return moveBaseNow(root,base2,head,version,keep,log3,dirty,!0);writeLog({level:"error",text:`Moving ${base2} for ${version}: ${err2.message}`}),log3.push({ok:!1,text:`The code on this computer was not updated. An engineer can bring it in with: git merge ${branch}`})}}async function pushAgain(bp,release){let root=projectRoot(bp),list2=release.unpushed??[],multi=list2.length>1||!!readMeta(bp).repos?.length,log3=[],unpushed=[];for(let u of list2){if(unpushed.some(x=>repoReliesOn(readMeta(bp),u.repo,x.repo))){unpushed.push(u);continue}let repoRoot=path79.join(root,u.repo),url=git(repoRoot,["config","--get",`remote.${u.remote}.url`],{allowFail:!0}),host=url?repoHostFor(bp,url):null,env2=host?.gitEnv()??{},lines3=[];try{await netGitLater(repoRoot,["fetch",u.remote,u.base],{env:env2,allowFail:!0});let there=git(repoRoot,["rev-parse","--verify","-q",`refs/remotes/${u.remote}/${u.base}`],{allowFail:!0});if(!there||!isAncestor(repoRoot,u.commit,there)){let sent=await pushRelease(repoRoot,u.remote,u.base,release.version,release.notes,host,env2,lines3,u.commit);sent!==u.commit&&await moveLocalBase(repoRoot,u.base,sent,release.version,keepOf(bp,repoRoot),lines3)}u.tag&&await netGitLater(repoRoot,["push",u.remote,`refs/tags/${u.tag}`],{env:env2}),lines3.push({ok:!0,text:"Sent to your team's shared copy of the code"})}catch(err2){let error2=err2.message,plain=pushWords(error2);unpushed.push({...u,error:pushDetail(error2,{remote:u.remote,base:u.base,version:release.version}),plain}),lines3.push({ok:!1,text:plain})}log3.push(...lines3.map(l=>multi?{...l,text:`${u.repo}: ${l.text}`}:l))}return{log:log3,unpushed}}function keepOf(bp,repoRoot){let keep=path79.relative(repoRoot,bp.squadDir);return keep&&!keep.startsWith("..")?keep:void 0}var PushClash=class extends Error{};function isBehind(message){return/fetch first|fast-forward|Updates were rejected|before pushing again|tip of your current branch is behind/i.test(message)}function isProtected(message){return/protected branch|GH00\d|GH013|remote rejected|pre-receive hook declined|through a pull request|merge request/i.test(message)}var SHARED_COPY="your team's shared copy of the code";function pushWords(message){return/same lines as this release/.test(message)?message.split(`
607
608
  `)[0]:/refs\/tags\/.*(already exists|rejected)|\[rejected\].*tag|tag already exists/i.test(message)?"A teammate released this version number while this release went out, so the shared copy did not take it. The release is safe on this computer. An engineer can see what to do below.":isProtected(message)?"The shared copy of the code only takes changes after a review, and squad-kit cannot ask for one there. The release is safe on this computer. An engineer can see what to do below. Press Send again once it is in.":/could not resolve|could not read from remote|does not appear to be|repository exists|unable to access|connection|timed out|gave up after|network|host key/i.test(message)?`squad-kit could not reach ${SHARED_COPY}, so the release is only on this computer. Check the connection, then press Send again.`:/authentication failed|permission denied|could not read username|\b40[13]\b/i.test(message)?"The shared copy of the code did not accept this computer's sign-in, so the release is only on this computer. Check the sign-in, then press Send again.":"The shared copy of the code did not take the release, so it is only on this computer. An engineer can see why below. Press Send again once that is settled."}async function versionOnRemote(bp,release,tickets){if(!release.addons.tag)return!1;let meta=readMeta(bp),root=projectRoot(bp),repos=new Map(tickets.flatMap(t=>copyViews(meta,t)).map(t=>{let r=repoFor(meta,t);return[r.path,r]}));for(let repo of repos.values()){let repoRoot=path79.join(root,repo.path),remote=repo.remote||remotes(repoRoot)[0];if(!remote)continue;let url=git(repoRoot,["config","--get",`remote.${remote}.url`],{allowFail:!0}),env2=url?repoHostFor(bp,url)?.gitEnv()??{}:{};if((await netGitLater(repoRoot,["ls-remote","--tags",remote,`refs/tags/${release.version}`],{env:env2,allowFail:!0})).trim())return!0}return!1}function pushDetail(message,at3){return/same lines as this release/.test(message)?message.split(`
@@ -1435,9 +1436,9 @@ ${by2} deleted ${t.id} "${t.title}" on their computer. squad-kit left this note
1435
1436
  `).map(l=>l.trim()).filter(Boolean)}function inUse(root){return new Set(lines(root,["worktree","list","--porcelain"]).filter(l=>l.startsWith("branch refs/heads/")).map(l=>l.slice(18)))}function releasedTicket(tickets,branch){return tickets.find(t=>t.stage==="done"&&(t.branch===branch||`squad/${t.id.toLowerCase()}`===branch))}function oldBranches(bp,only){let meta=readMeta(bp),tickets=listTickets(bp).filter(t=>!only||only.includes(t.id)),plan={here:[],remote:[],kept:[]};for(let repo of reposOf(meta)){let root=path90.join(bp.root,repo.path);if(!isRepo(root))continue;let base2=repo.base,used=inUse(root);for(let branch of lines(root,["for-each-ref","--format=%(refname:short)","refs/heads/squad"])){let t=releasedTicket(tickets,branch);t&&(used.has(branch)?plan.kept.push({branch,why:"a copy of the code still uses it"}):isAncestor(root,branch,base2)?plan.here.push({repo:repo.path,branch,ticket:t.id,sha:git(root,["rev-parse",branch])}):plan.kept.push({branch,why:`it has work that is not in ${base2}`}))}let remote=repo.remote||remotes(root)[0],there=remote?git(root,["rev-parse","--verify","-q",`refs/remotes/${remote}/${base2}`],{allowFail:!0}):"";if(!(!remote||!there))for(let ref2 of lines(root,["for-each-ref","--format=%(refname)",`refs/remotes/${remote}/squad`])){let branch=ref2.slice(`refs/remotes/${remote}/`.length),t=releasedTicket(tickets,branch);t&&(isAncestor(root,ref2,there)?plan.remote.push({repo:repo.path,branch,ticket:t.id,remote,sha:git(root,["rev-parse",ref2])}):plan.kept.push({branch:`${remote}/${branch}`,why:`it has work that is not in ${remote}/${base2}`}))}}return plan}async function cleanBranches(bp,opts={}){if(!opts.remote)return cleanBranchesHere(bp,opts);for(let repo of reposOf(readMeta(bp))){let root=path90.join(bp.root,repo.path),remote=isRepo(root)?repo.remote||remotes(root)[0]:void 0;remote&&await netGitLater(root,["fetch","--prune",remote],{env:hostEnv(bp,root,remote),allowFail:!0})}let plan=oldBranches(bp,opts.only),{deleted,failed:failed2}=deleteHere(bp,plan);for(let b of plan.remote){let root=path90.join(bp.root,b.repo);try{await netGitLater(root,["push",`--force-with-lease=refs/heads/${b.branch}:${b.sha}`,b.remote,"--delete",b.branch],{env:hostEnv(bp,root,b.remote)}),deleted.push(`${b.remote}/${b.branch}`)}catch(err2){failed2.push(`${b.remote}/${b.branch}: ${err2.message}`)}}return{deleted,failed:failed2,remoteLeft:[],kept:plan.kept}}function cleanBranchesHere(bp,opts={}){let plan=oldBranches(bp,opts.only);return{...deleteHere(bp,plan),remoteLeft:plan.remote,kept:plan.kept}}function deleteHere(bp,plan){let deleted=[],failed2=[];for(let b of plan.here){let root=path90.join(bp.root,b.repo);try{git(root,["update-ref","-d",`refs/heads/${b.branch}`,b.sha]),deleted.push(b.branch)}catch(err2){failed2.push(`${b.branch}: ${err2.message}`)}}return{deleted,failed:failed2}}function hostEnv(bp,root,remote){let url=git(root,["config","--get",`remote.${remote}.url`],{allowFail:!0});return url?repoHostFor(bp,url)?.gitEnv()??{}:{}}import fs98 from"fs";function draftFor(bp,version){let r=readRelease(bp,version);if(!r||r.status!=="draft")throw new BoardActionError(`${version} is not a release that waits to go out.`);if(releaseGoingOut(bp)===r.version)throw new BoardActionError(`${r.version} is going out right now. Wait for it to finish.`);return r}function takeOutOfRelease(bp,version,id2){return takingOutTurn(bp,()=>takeOut2(bp,version,id2))}async function takeOut2(bp,version,id2){let asked=draftFor(bp,version),t=readTicket(bp,id2);if(!t||!asked.tickets.includes(t.id))throw new BoardActionError(`${id2.toUpperCase()} is not in ${asked.version}.`);if(asked.tickets.length===1)throw new BoardActionError(`${t.id} is the only ticket in ${asked.version}. Leave it in, or simply release later.`);let needs=listTickets(bp).filter(x=>asked.tickets.includes(x.id)&&x.id!==t.id&&x.dependsOn?.includes(t.id));if(needs.length)throw new BoardActionError(`${needs.map(x=>x.id).join(" and ")} in this release ${needs.length===1?"depends":"depend"} on ${t.id}. Take ${needs.length===1?"it":"them"} out first, or keep ${t.id} in.`);let meta=readMeta(bp);if(meta.engine==="live"&&t.mergedAt){let repo=repoFor(meta,t),problem2=await leaveOutProblem(path91.join(bp.root,repo.path),repo.base,[t,...(asked.out??[]).map(x=>readTicket(bp,x)).filter(x=>!!x?.mergedAt&&repoFor(meta,x).path===repo.path)]);if(problem2)throw new BoardActionError(`${problem2} Keep it in ${asked.version}.`)}let r=draftFor(bp,version),next={...r,tickets:r.tickets.filter(x=>x!==t.id),out:[...new Set([...r.out??[],t.id])]};return writeRelease(bp,next),updateTicket(bp,t.id,x=>{let base2=repoFor(meta,x).base;x.status=`Taken out of ${r.version} \xB7 goes out in the release after it`,addActivity(x,"you",x.mergedAt?`Took it out of ${r.version}: its code stays in ${base2}, and ${r.version} leaves it out`:`Took it out of ${r.version}`)}),next}function putBackInRelease(bp,version,id2){let r=draftFor(bp,version),key=id2.toUpperCase();if(!r.out?.includes(key))throw new BoardActionError(`${key} was not taken out of ${r.version}.`);let out18=r.out.filter(x=>x!==key),next={...r,tickets:[...new Set([...r.tickets,key])],out:out18};return out18.length||delete next.out,writeRelease(bp,next),readTicket(bp,key)&&updateTicket(bp,key,x=>{x.release=r.version,x.status=x.mergedAt?`Merged into ${repoFor(readMeta(bp),x).base} \xB7 goes out in ${r.version}`:`Ready for ${r.version}`,addActivity(x,"you",`Put it back into ${r.version}`)}),next}function afterRelease(bp,release){let out18=(release.out??[]).filter(id2=>!release.tickets.includes(id2)&&readTicket(bp,id2));if(out18.length){joinNextRelease(bp,out18);for(let id2 of out18)updateTicket(bp,id2,x=>{x.status=x.mergedAt?`Merged into ${repoFor(readMeta(bp),x).base} \xB7 goes out in ${x.release}`:`Ready for ${x.release}`})}if(readMeta(bp).engine!=="live")return;let lines3=[];try{let done=cleanBranchesHere(bp,{only:release.tickets});done.deleted.length&&lines3.push({ok:!0,text:"Deleted what was left over from released tickets on this computer"});for(let f of done.failed)lines3.push({ok:!1,text:`Could not delete ${f}`});done.remoteLeft.length&&lines3.push({ok:!0,text:`What is left of them in the shared copy of the code stays (${done.remoteLeft.length}). To delete it too: squad branches --clean --remote`})}catch(err2){lines3.push({ok:!1,text:`Could not tidy what was left over: ${plainWords(err2,"Tidying after a release","something technical stopped it. An engineer can see why in the log.")}`})}lines3.length&&(release.log=[...release.log??[],...lines3],writeRelease(bp,release))}async function settleRelease(bp,why){let j=readJournal(bp);if(!j)return null;let release=readRelease(bp,j.version);if(!release||release.status==="released")return clearJournal(bp),release;let meta=readMeta(bp),repos=await settleRepos(bp,j);for(let id2 of j.prMerged??[])readTicket(bp,id2)&&updateTicket(bp,id2,x=>{x.mergedAt||(x.mergedAt=now(),addActivity(x,"release",`Its pull request was merged while ${j.version} went out`))});let reached=new Set(repos.filter(r=>r.reached).map(r=>r.repo)),log3=[{ok:!1,text:why},...repos.flatMap(r=>repos.length>1||meta.repos?.length?r.log.map(l=>({...l,text:`${r.repo}: ${l.text}`})):r.log)];if(clearJournal(bp),!reached.size)return writeRelease(bp,{...release,log:[...log3,{ok:!0,text:`${release.version} did not go out. Press Release again when you are ready.`}]}),readRelease(bp,release.version);let going=j.tickets.filter(id2=>{let t=readTicket(bp,id2);return t&&copyViews(meta,t).every(v=>reached.has(repoFor(meta,v).path))}),rest=release.tickets.filter(id2=>!going.includes(id2)),unpushed=repos.flatMap(r=>r.unpushed?[r.unpushed]:[]);release.addons.pipeline&&(meta.commands?.deploy||meta.repos?.some(x=>x.commands.deploy))&&log3.push({ok:!1,text:"Your after-release step did not run, because the release was cut off. Run it yourself when you are ready."});let done={...release,status:"released",date:release.date??j.at,tickets:going,log:log3,...unpushed.length?{unpushed}:{}};unpushed.length||delete done.unpushed,writeRelease(bp,done);for(let id2 of going){let t=readTicket(bp,id2);for(let v of t?.worktree?copyViews(meta,t):[])await removeWorktree(path91.join(bp.root,repoFor(meta,v).path),v.worktree);t?.storyDir&&!copyDirsIn(t.storyDir).length&&fs98.rmSync(t.storyDir,{recursive:!0,force:!0}),dropTicketCache(bp,id2)}return rest.length&&joinNextRelease(bp,rest),closeReleased(bp,done),afterRelease(bp,done),readRelease(bp,done.version)}async function resumeCutRelease(bp){try{let j=readJournal(bp);if(!j||journalAlive(j)||releaseGoingOut(bp))return null;let free=holdRelease(bp,j.version);try{return await settleRelease(bp,`${j.version} was cut off while it went out (the app closed or the computer restarted).`)}finally{free()}}catch{return null}}function nextDraft(bp,version){let v=version&&(version.startsWith("v")?version:`v${version}`),going=releaseGoingOut(bp),r=listReleases(bp).find(x=>x.status==="draft"&&x.version!==going&&(!v||x.version===v));if(!r)throw new BoardActionError(v?`${v} is not a release that waits to go out.`:"There is no release ready to go out.");return r}function outItems(bp,r){return(r.out??[]).map(id2=>{let t=readTicket(bp,id2);return{id:id2,title:t?.title??r.titles?.[id2]??id2,merged:!!(t&&isMerged(t))}})}import fs105 from"fs";import path98 from"path";import{randomBytes as randomBytes5}from"crypto";import fs99 from"fs";import os15 from"os";import path92 from"path";import{randomUUID as randomUUID5}from"crypto";async function watchLane(dir,names2,wholeCopy=!1){let settings=hookSettings(dir),link=gitLinkOf(dir),lane={dir,...placeOf(dir),names:names2,kept:await readKept(dir,names2),...settings?{hookSettings:settings}:{},...link?{gitLink:link}:{}};if(wholeCopy){let tree=await treeOf(dir);tree&&(lane.tree=tree)}return lane}async function checkLane(lane,skip=[]){let out18={restored:[],putBack:[]},{dir}=lane;if(!fs99.existsSync(dir))return out18;let relinked=putGitLinkBack(lane),{head,branch}=placeOf(dir);return relinked?out18.moved="the copy's link to its git folder was changed":branch!==lane.branch?out18.moved=branch?`the copy is on ${branch}, not ${lane.branch}`:`the copy left ${lane.branch}`:head!==lane.head&&(out18.moved=`${branch||"HEAD"} moved from ${head8(lane.head)} to ${head8(head)}`),lane.tree&&!out18.moved&&(out18.putBack=await putTreeBack(dir,lane.tree,skip)),out18.restored=[...putKeptBack(dir,lane.kept,await readKept(dir,lane.names)),...putSettingsBack(dir,lane.hookSettings)],out18}function gitLinkOf(dir){try{let file3=path92.join(dir,".git");return fs99.lstatSync(file3).isFile()?fs99.readFileSync(file3):void 0}catch{return}}function putGitLinkBack(lane){if(!lane.gitLink||gitLinkOf(lane.dir)?.equals(lane.gitLink))return!1;let file3=path92.join(lane.dir,".git");return fs99.rmSync(file3,{force:!0,recursive:!0}),fs99.writeFileSync(file3,lane.gitLink),!0}function readPackage(dir){try{let json2=JSON.parse(fs99.readFileSync(path92.join(dir,"package.json"),"utf8"));return json2&&typeof json2=="object"&&!Array.isArray(json2)?json2:null}catch{return null}}function hookSettings(dir){let pkg=readPackage(dir),kept=Object.fromEntries(HOOK_PACKAGE_KEYS.filter(k=>pkg?.[k]!==void 0).map(k=>[k,pkg[k]]));return Object.keys(kept).length?JSON.stringify(kept):void 0}function putSettingsBack(dir,before){let after=hookSettings(dir);if(after===before)return[];let pkg=readPackage(dir);if(!pkg)return[];let was=before?JSON.parse(before):{},now2=after?JSON.parse(after):{},changed=HOOK_PACKAGE_KEYS.filter(k=>JSON.stringify(was[k])!==JSON.stringify(now2[k]));for(let k of changed)was[k]===void 0?delete pkg[k]:pkg[k]=was[k];let file3=path92.join(dir,"package.json"),indent=/^\{\r?\n([ \t]+)/.exec(fs99.readFileSync(file3,"utf8"))?.[1]??" ";return fs99.writeFileSync(file3,`${JSON.stringify(pkg,null,indent)}
1436
1437
  `),changed.map(k=>`package.json (${k})`)}function placeOf(dir){let[head="",name=""]=git(dir,["rev-parse","HEAD","--abbrev-ref","HEAD"],{allowFail:!0}).split(`
1437
1438
  `);return{head,branch:name==="HEAD"?"":name}}function head8(sha2){return sha2?sha2.slice(0,8):"nothing"}function listed(out18){return out18.split("\0").filter(Boolean)}var HOOK_RUNNER=/(?:^|\/)\.husky\/_\//;async function protectedPaths(dir,names2){let top=names2.map(n=>n.replace(/^\//,"").replace(/\/$/,"")),deep=names2.filter(n=>!n.startsWith("/")).map(n=>n.endsWith("/")?`:(glob)**/${n}**`:`:(glob)**/${n}`),ls=async args=>listed(await gitLater(dir,["ls-files","-z","--cached","--others",...args],{allowFail:!0}));return[...new Set([...await ls(["--",...top]),...deep.length?await ls(["--exclude-standard","--",...deep]):[]])].filter(f=>!HOOK_RUNNER.test(f))}async function readKept(dir,names2){let kept=new Map;if(!names2.length)return kept;for(let rel of await protectedPaths(dir,names2)){let file3=path92.join(dir,rel);try{let st=fs99.lstatSync(file3);st.isSymbolicLink()?kept.set(rel,{link:fs99.readlinkSync(file3)}):st.isFile()&&kept.set(rel,{data:fs99.readFileSync(file3),mode:st.mode&511})}catch{}}return kept}function same2(a,b){return!a||!b?a===b:"link"in a||"link"in b?"link"in a&&"link"in b&&a.link===b.link:a.data.equals(b.data)&&a.mode===b.mode}function putKeptBack(dir,before,after){let added2=[...after.keys()].filter(rel=>!before.has(rel)),changed=[...before.keys()].filter(rel=>!same2(before.get(rel),after.get(rel)));for(let rel of added2)removeFile(dir,rel);for(let rel of changed){let was=before.get(rel),file3=path92.join(dir,rel);makeParents(dir,rel),fs99.rmSync(file3,{force:!0,recursive:!0}),"link"in was?fs99.symlinkSync(was.link,file3):(fs99.writeFileSync(file3,was.data),fs99.chmodSync(file3,was.mode))}return[...changed,...added2].sort()}function makeParents(dir,rel){let at3=dir;for(let part of path92.dirname(rel).split(path92.sep).filter(p=>p&&p!==".")){at3=path92.join(at3,part);try{fs99.lstatSync(at3).isDirectory()||fs99.rmSync(at3,{force:!0})}catch{}fs99.mkdirSync(at3,{recursive:!0})}}function removeFile(dir,rel){fs99.rmSync(path92.join(dir,rel),{force:!0,recursive:!0});for(let d=path92.dirname(rel);d&&d!==".";d=path92.dirname(d))try{fs99.rmdirSync(path92.join(dir,d))}catch{break}}async function treeOf(dir){let tmp=path92.join(os15.tmpdir(),`squad-index-${randomUUID5()}`);try{let index=path92.resolve(dir,git(dir,["rev-parse","--git-path","index"]));if(fs99.existsSync(index)){fs99.copyFileSync(index,tmp);let st=fs99.statSync(index);fs99.utimesSync(tmp,st.atime,st.mtime)}let env2={GIT_INDEX_FILE:tmp};return await stageAllLater(dir,env2),await gitLater(dir,["write-tree"],{env:env2})||void 0}catch{return}finally{fs99.rmSync(tmp,{force:!0})}}async function putTreeBack(dir,before,skip){let after=await treeOf(dir);if(!after||after===before)return[];let rows2=listed(git(dir,["diff","--name-status","--no-renames","-z",before,after])),added2=[],back=[];for(let i=0;i+1<rows2.length;i+=2){let rel=rows2[i+1];skip.some(s=>isWithin(s,path92.join(dir,rel)))||(rows2[i]==="A"?added2:back).push(rel)}for(let rel of added2)removeFile(dir,rel);for(let i=0;i<back.length;i+=100)await gitLater(dir,["restore",`--source=${before}`,"--worktree","--",...back.slice(i,i+100)],{env:{GIT_LITERAL_PATHSPECS:"1"}});return[...back,...added2].sort()}function isWithin(folder,file3){let r=path92.relative(folder,file3);return r===""||!r.startsWith("..")&&!path92.isAbsolute(r)}import fs104 from"fs";import path97 from"path";import fs100 from"fs";import path93 from"path";var DOCKER_FILE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$|^Dockerfile(?:[.-][\w.-]+)?$|\.dockerfile$/i,MAX_DOCKER_FILES=30,storyFilesDir=bp=>path93.join(bp.localDir,"story-files"),storyFile2=(bp,f)=>path93.join(storyFilesDir(bp),f.repo==="."?"_project":f.repo,f.path),same3=(a,b)=>a.repo===b.repo&&a.path===b.path,literal=file3=>`:(literal)${file3}`;function isTracked(bp,f){return git(path93.join(bp.root,f.repo),["ls-files","--",literal(f.path)],{allowFail:!0})!==""}function fileFrom(bp,meta,repo,file3){let repos=repoPaths(meta),r=repo?.trim()||(repos.length===1?repos[0]:"");if(!repos.includes(r))throw new Error(r?`There is no code repo at ${r}. The repos: ${repos.join(", ")}.`:`Say which repo it is in: ${repos.join(", ")}.`);let text3=(file3??"").trim().replace(/\\/g,"/"),parts2=text3.split("/").filter(x=>x&&x!==".");if(!parts2.length)throw new Error("Write the file's path inside the repo, like .env.dev.");if(text3.startsWith("/")||/^[a-z]:/i.test(text3)||parts2.includes(".."))throw new Error("The file must be inside the repo, like public/config.js.");if(parts2.includes(".git")||parts2.includes(SQUAD_DIR))throw new Error(`Agents already cannot change ${text3}.`);let clean4=parts2.join("/"),onDisk=path93.join(bp.root,r,clean4);if(text3.endsWith("/")||fs100.existsSync(onDisk)&&fs100.statSync(onDisk).isDirectory())throw new Error(`${clean4} is a folder. Name each file in it.`);return{repo:r,path:clean4}}var listed2=(meta,kind)=>(kind==="local"?meta.localFiles:meta.lockedFiles)??[];function withList(meta,kind,files){let next={...meta},key=kind==="local"?"localFiles":"lockedFiles";return files.length?next[key]=files:delete next[key],next}function added(meta,kind,f){return withList(meta,kind,[...listed2(meta,kind).filter(x=>!same3(x,f)),f])}function addLocalFile(bp,repo,file3){let meta=readMeta(bp),f=fileFrom(bp,meta,repo,file3);if(isTracked(bp,f))throw new Error(`${f.path} is in git, so every copy has it. Lock it instead, or take it out of git first.`);if(!git(path93.join(bp.root,f.repo),["check-ignore","--",f.path],{allowFail:!0}))throw new Error(`git would save ${f.path}. Add it to the repo's .gitignore first.`);return writeMeta(bp,added(withList(meta,"locked",listed2(meta,"locked").filter(x=>!same3(x,f))),"local",f)),f}function addLockedFile(bp,repo,file3){let meta=readMeta(bp),f=fileFrom(bp,meta,repo,file3);if(!isTracked(bp,f))throw new Error(`${f.path} is not in git. A file git does not carry is a local file.`);return writeMeta(bp,added(meta,"locked",f)),f}function dockerFiles(bp,meta){let out18=[];for(let repo of repoPaths(meta)){let files=git(path93.join(bp.root,repo),["ls-files","-z"],{allowFail:!0}).split("\0").filter(Boolean);for(let file3 of files)DOCKER_FILE.test(path93.basename(file3))&&!listed2(meta,"locked").some(x=>same3(x,{repo,path:file3}))&&out18.push({repo,path:file3})}return out18.slice(0,MAX_DOCKER_FILES)}function lockDockerFiles(bp){let meta=readMeta(bp),found2=dockerFiles(bp,meta);if(!found2.length)throw new Error("There is no Docker file to lock.");return writeMeta(bp,withList(meta,"locked",[...listed2(meta,"locked"),...found2])),found2}function removeProjectFile(bp,repo,file3){let meta=readMeta(bp),f=fileFrom(bp,meta,repo,file3),kind=listed2(meta,"local").some(x=>same3(x,f))?"local":listed2(meta,"locked").some(x=>same3(x,f))?"locked":void 0;if(!kind)throw new Error(`${f.path} is not on the list.`);return writeMeta(bp,withList(meta,kind,listed2(meta,kind).filter(x=>!same3(x,f)))),kind==="local"&&fs100.rmSync(storyFile2(bp,f),{force:!0}),f}function useMyFile(bp,repo,file3){let meta=readMeta(bp),f=fileFrom(bp,meta,repo,file3);if(!listed2(meta,"local").some(x=>same3(x,f)))throw new Error(`${f.path} is not a local file yet.`);let from=path93.join(bp.root,f.repo,f.path);if(!fs100.existsSync(from))throw new Error(`There is no ${f.path} in your folder.`);let to=storyFile2(bp,f);return fs100.mkdirSync(path93.dirname(to),{recursive:!0,mode:448}),fs100.copyFileSync(from,to),fs100.chmodSync(to,384),f}function projectFilesView(bp){let meta=readMeta(bp);return{local:listed2(meta,"local").map(f=>({...f,ready:fs100.existsSync(storyFile2(bp,f)),inFolder:fs100.existsSync(path93.join(bp.root,f.repo,f.path)),storyFile:storyFile2(bp,f)})),locked:listed2(meta,"locked"),repos:repoPaths(meta),docker:dockerFiles(bp,meta)}}function offLimits(meta){return[...listed2(meta,"local"),...listed2(meta,"locked")].map(f=>f.path)}import{execFileSync as execFileSync12}from"child_process";import{createHash as createHash9}from"crypto";import fs103 from"fs";import net3 from"net";import path96 from"path";function projectApps(meta){let all=reposOf(meta).filter(r=>repoRuns(meta,r.path)).map(r=>({name:r.path==="."?meta.project:repoLabel(meta,r.path)??r.path,repo:r.path,base:r.base,...r.commands.url?.trim()?{url:r.commands.url.trim()}:{},...r.commands.start?.trim()?{start:r.commands.start.trim()}:{},...r.commands.setup?.trim()?{setup:r.commands.setup.trim()}:{}})),known=all.filter(a=>a.url||a.start),noCode=meta.scan?.empty||meta.setup?.kind==="empty"&&meta.scan?.status!=="done";return known.length||meta.engine!=="live"||noCode?known:all}function findApp(meta,ref2){let apps2=projectApps(meta);if(!apps2.length)throw new Error("This project has no app to run yet. Set its start command in Settings, under How the squad checks and runs this project.");let asked=ref2?.trim().toLowerCase();if(!asked){if(apps2.length===1)return apps2[0];throw new Error(`This project has ${apps2.length} apps: ${apps2.map(a=>a.name).join(", ")}. Name one, for example: squad app start ${apps2[0].name}`)}let app=apps2.find(a=>a.name.toLowerCase()===asked||a.repo.toLowerCase()===asked);if(!app)throw new Error(`There is no app called "${ref2}". This project's apps: ${apps2.map(a=>a.name).join(", ")}.`);return app}function withAppCommands(meta,repo,set2){let start=set2.start?.trim(),url=set2.url?.trim();if(!start&&!url)throw new Error("Type the command that starts the app, for example npm run dev.");if(url&&!/^https?:\/\/[^\s/]+/i.test(url))throw new Error(`"${url}" is not a web address. Type it like http://localhost:3000`);let merge=(c={})=>({...c,...start?{start}:{},...url?{url}:{}});if(meta.repos?.length){if(!meta.repos.some(r=>r.path===repo))throw new Error(`There is no code repo at ${repo}.`);return{...meta,repos:meta.repos.map(r=>r.path===repo?{...r,commands:merge(r.commands)}:r)}}return{...meta,commands:merge(meta.commands)}}var NEW_FOR_MS=1440*6e4;function appNews(meta,tickets,app,notNow=[],now2=Date.now()){let merged2=tickets.filter(t=>t.mergedAt&&copyViews(meta,t).some(v=>repoFor(meta,v).path===app.repo)).sort((a,b)=>b.mergedAt.localeCompare(a.mergedAt)),runs2=app.state==="running"||app.state==="starting",since=app.startedAt,includes=runs2&&since?merged2.filter(t=>t.mergedAt<=since).slice(0,3).map(t=>({id:t.id,title:t.title})):[];if(app.state==="starting")return{includes};let fresh=merged2.find(t=>(!since||t.mergedAt>since)&&(runs2||now2-Date.parse(t.mergedAt)<NEW_FOR_MS)&&!notNow.includes(t.id));if(!fresh)return{includes};let base2=repoFor(meta,fresh).base;return{includes,fresh:{id:fresh.id,title:fresh.title,base:base2,...fresh.release?{release:fresh.release}:{}}}}async function isUp(url){for(let u of new Set([url,url.replace("://localhost","://127.0.0.1")]))try{return await fetch(u,{redirect:"manual",signal:AbortSignal.timeout(1500)}),!0}catch{}return!1}import{createHash as createHash8}from"crypto";import fs101 from"fs";import os16 from"os";import path94 from"path";var DOCKER_WAIT_MS=20*6e4,DOCKER_STOP_MS=2*6e4,OWN_COMPOSE_FILES=["compose.yaml","compose.yml","docker-compose.yaml","docker-compose.yml"],OWN_OVERRIDES=["compose.override.yaml","compose.override.yml","docker-compose.override.yaml","docker-compose.override.yml"],VALUE_FLAGS=new Set(["-f","--file","--env-file","--profile","--parallel","--ansi","--progress","--project-directory"]),isCompose=command=>/^\s*(docker\s+compose|docker-compose)(\s|$)/.test(command);function dockerSocket(env2=process.env,home2=os16.homedir(),system="/var/run/docker.sock"){let named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];for(let place of[named,path94.join(home2,".docker","run","docker.sock"),system])if(place)try{return fs101.realpathSync(place)}catch{}return null}function composeRun(meta,repo,command,dir,root){if(!isCompose(command))return null;let cannot=why=>({why:`squad-kit cannot run this part's Docker command: ${why}`});if(/[;&|`$<>\n()\\'"]/.test(command))return cannot("it must be one plain docker compose command, with nothing else on the line.");let words2=command.trim().split(/\s+/),start=words2[0]==="docker"?2:1,up=words2.indexOf("up",start);if(up<0)return cannot("it must be docker compose up.");let named=[];for(let i=start;i<up;i+=1){let w=words2[i];if(w==="-p"||w.startsWith("--project-name"))return cannot("squad-kit gives it its own project name, so it must not name one.");if(w==="--project-directory"||w.startsWith("--project-directory="))return cannot("it must run from its own folder.");w==="-f"||w==="--file"?named.push(words2[i+=1]??""):w.startsWith("--file=")?named.push(w.slice(7)):VALUE_FLAGS.has(w)&&(i+=1)}let inFolder=f=>fs101.existsSync(path94.join(dir,f)),files=named.length?named:[...OWN_COMPOSE_FILES.filter(inFolder).slice(0,1),...OWN_OVERRIDES.filter(inFolder)];if(!files.length)return cannot("it has no compose file.");let safe2=(meta.lockedFiles??[]).concat(meta.localFiles??[]).filter(f=>f.repo===repo).map(f=>f.path),open3=files.filter(f=>!safe2.includes(path94.normalize(f)));if(open3.length)return cannot(`${open3.join(", ")} ${open3.length===1?"is":"are"} not locked, so a story could change what it mounts. An engineer can lock the Docker files in Settings.`);let socket=dockerSocket();if(!socket)return cannot("Docker is not running on this computer, or squad-kit cannot find it.");let name=`squad-${createHash8("sha1").update(`${root}
1438
- ${repo}`).digest("hex").slice(0,10)}`,config2=path94.join(squadHome(),"docker-config");fs101.mkdirSync(config2,{recursive:!0});let before=words2.slice(0,up),after=words2.slice(up+1).filter(w=>w!=="-d"&&w!=="--detach");return{command:[...before,"up",...after].join(" "),stop:[...before,"stop"].join(" "),env:{COMPOSE_PROJECT_NAME:name,DOCKER_HOST:`unix://${socket}`,DOCKER_CONFIG:config2}}}function dockerGuard(guard,run6){let socket=run6.env.DOCKER_HOST.replace(/^unix:\/\//,""),under=(place,base2)=>place===base2||place.startsWith(`${base2}/`);return{...guard,noConnect:guard.noConnect.filter(s=>!under(socket,s)&&!under(s,socket)),openSocket:socket,read:[...guard.read,run6.env.DOCKER_CONFIG],write:[...guard.write,run6.env.DOCKER_CONFIG]}}import fs102 from"fs";import path95 from"path";var lockFile2=bp=>path95.join(bp.localDir,"product-lock.json"),alive=pid=>{try{return process.kill(pid,0),!0}catch(err2){return err2.code==="EPERM"}};function productHold(bp){try{let hold=JSON.parse(fs102.readFileSync(lockFile2(bp),"utf8"));if(alive(hold.pid))return hold;fs102.rmSync(lockFile2(bp),{force:!0})}catch{}return null}function takeProduct(bp,holder,kind){let now2=productHold(bp);if(now2?.holder===holder)return null;if(now2)return now2;try{return fs102.mkdirSync(bp.localDir,{recursive:!0}),fs102.writeFileSync(lockFile2(bp),JSON.stringify({holder,kind,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"}),null}catch{return productHold(bp)??{holder:"another run",kind:"story",pid:process.pid,at:new Date().toISOString()}}}function leaveProduct(bp,holder){productHold(bp)?.holder===holder&&fs102.rmSync(lockFile2(bp),{force:!0})}var WAIT_LIMIT_MS=45*6e4,LOOK_EVERY_MS=2e3;async function waitForProduct(bp,holder,kind,o={}){let until=Date.now()+(o.limitMs??WAIT_LIMIT_MS),said=!1;for(;;){let other=takeProduct(bp,holder,kind);if(!other)return;if(said||(said=!0,o.onWait?.(other)),other.wanted!==holder)try{fs102.writeFileSync(lockFile2(bp),JSON.stringify({...other,wanted:holder}))}catch{}if(o.signal?.aborted)throw new Error("You stopped the squad.");if(Date.now()>until)throw new Error(`${other.holder} kept the running product too long`);await new Promise(r=>setTimeout(r,o.lookMs??LOOK_EVERY_MS))}}var CANNOT_START="squad-kit could not start it.",APP_WAIT_MS=3*6e4,SETUP_MS=20*6e4,OUTPUT_KEEP=6e4,YOUR_APPS="Your apps";function appCopyDir(bp,app){return path96.join(bp.localDir,"apps",app.name.replace(/[^\w.-]+/g,"-")||"app")}async function updateCopy(root,repo,dir,how={}){let src=path96.join(root,repo.path),commit=latestBase(src,repo.base,repo.remote||void 0).sha;return copyTurn(dir,async()=>{if(await clearHalfCopy(src,dir,how.signal),copyIsWhole(dir))try{return await gitLater(dir,["checkout","--force","--detach",commit],how.signal?{signal:how.signal}:{}),{commit,fresh:!1}}catch(err2){if(err2 instanceof GitStopped)throw err2}return await makeCopy(src,dir,["--force","--detach"],commit,how),{commit,fresh:!0}})}var ENV_FILE2=/^\.env(\.[\w.-]+)?$/,ENV_EXAMPLE=/\.(example|sample|template|dist)$/;async function localEnvFiles(src){let found2=new Set,list2=args=>gitRecords(src,["ls-files","-z","--others","--exclude-standard",...args],"\0",f=>{let name=path96.basename(f);!f.endsWith("/")&&ENV_FILE2.test(name)&&!ENV_EXAMPLE.test(name)&&found2.add(f)}).catch(()=>{});return await list2([]),await list2(["--ignored","--directory"]),[...found2]}async function copyEnvFiles(src,dir,named=[]){let files=[...new Set([...await localEnvFiles(src),...named.filter(f=>fs103.existsSync(path96.join(src,f)))])];for(let f of files){let to=path96.join(dir,f);fs103.mkdirSync(path96.dirname(to),{recursive:!0}),fs103.copyFileSync(path96.join(src,f),to),fs103.chmodSync(to,384)}return files.sort()}var DEP_FILES=new Set(["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","bun.lock","bun.lockb","pyproject.toml","poetry.lock","Pipfile","Pipfile.lock","uv.lock","setup.py","setup.cfg","Gemfile","Gemfile.lock","go.mod","go.sum","Cargo.toml","Cargo.lock","composer.json","composer.lock","pom.xml","build.gradle","build.gradle.kts","packages.lock.json","Directory.Packages.props","mix.exs","mix.lock","pubspec.yaml","pubspec.lock"]),DEP_NAMES=[/^requirements.*\.txt$/,/\.(cs|fs|vb)proj$/];async function dependencyPrint(dir,commit){let lines3=[];return await gitRecords(dir,["ls-tree","-r",commit],`
1439
- `,l=>{let name=path96.basename(l.split(" ")[1]??"");(DEP_FILES.has(name)||DEP_NAMES.some(p=>p.test(name)))&&lines3.push(l)}).catch(()=>{}),createHash9("sha1").update(lines3.join(`
1440
- `)).digest("hex").slice(0,12)}function onPort(command,from,to){if(/\$\{?PORT\b|%PORT%/.test(command))return command;let n=String(from),settings=[new RegExp(`(--port[= ])${n}\\b`),new RegExp(`(\\s-p ?)${n}\\b`),new RegExp(`(\\bPORT=)${n}\\b`),new RegExp(`((?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1?\\]):)${n}\\b`)];for(let s of settings)if(s.test(command))return command.replace(s,`$1${to}`);return null}function portOf2(url){let u=new URL(url);return Number(u.port)||(u.protocol==="https:"?443:80)}function withPort(url,port){let u=new URL(url);return u.port=String(port),url.endsWith("/")?u.toString():u.toString().replace(/\/$/,"")}var LOCAL2=new Set(["localhost","127.0.0.1","0.0.0.0","[::1]","[::]"]);function isLocal(url){try{return LOCAL2.has(new URL(url).hostname)}catch{return!1}}function connects(host,port){return new Promise(resolve2=>{let s=net3.connect({host,port}),done=yes=>{s.destroy(),resolve2(yes)};s.setTimeout(800,()=>setImmediate(()=>done(!1))),s.once("connect",()=>done(!0)),s.once("error",()=>done(!1))})}async function portTaken(port){return await connects("127.0.0.1",port)||await connects("::1",port)}async function nextFreePort(from){for(let p=from+1;p<from+50&&p<65536;p+=1)if(!await portTaken(p))return p}function whoListens(port){if(process.platform==="win32")return null;try{let out18=execFileSync12("lsof",["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-Fpc"],{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:3e3}),pid=Number(/^p(\d+)/m.exec(out18)?.[1]);return pid?{pid,program:/^c(.+)$/m.exec(out18)?.[1]??""}:null}catch{return null}}function psField(pid,field){if(process.platform==="win32")return null;try{return Number(execFileSync12("ps",["-o",`${field}=`,"-p",String(pid)],{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:3e3}).trim())||null}catch{return null}}var sleep3=ms2=>new Promise(r=>setTimeout(r,ms2));function addressIn(output){return/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\]):\d{2,5}[^\s'"]*/i.exec(output.replace(/\x1b\[[0-9;]*m/g,""))?.[0].replace("0.0.0.0","127.0.0.1").replace(/\/$/,"")}function busyIn(output){let m=/(?:EADDRINUSE|address already in use)[^\n]*?:(\d{2,5})\b|port (\d{2,5}) is (?:already )?in use/i.exec(output);return m?Number(m[1]??m[2]):void 0}function squadKitOn(bp,port){let holder=whoListens(port);if(!holder)return null;let group=psField(holder.pid,"pgid")??holder.pid,records={};try{records=JSON.parse(fs103.readFileSync(path96.join(bp.localDir,"apps.json"),"utf8"))}catch{}let repo=Object.entries(records).find(([,r])=>r.pid===group)?.[0];if(repo!==void 0){let app=projectApps(readMeta(bp)).find(a=>a.repo===repo);return{group,holder,...app?{app:app.name}:{}}}return psField(group,"ppid")===process.pid?{group,holder}:null}var AppError=class extends Error{},AppRunner=class{constructor(bp,opts={}){this.bp=bp;this.opts=opts}bp;opts;runs=new Map;output=new Map;guardFor(cwd){if(!guardSupport().on)return;let allow=readMeta(this.bp).guard;return projectGuardFor(this.bp,{cwd,...allow?{allow}:{}})}find(ref2){try{return findApp(readMeta(this.bp),ref2)}catch(err2){throw new AppError(err2.message)}}repoOf(app){return reposOf(readMeta(this.bp)).find(r=>r.path===app.repo)}file(){return path96.join(this.bp.localDir,"apps.json")}records(){try{return JSON.parse(fs103.readFileSync(this.file(),"utf8"))}catch{return{}}}record(repo,change){let all=this.records(),r=all[repo]??{};change(r),all[repo]=r,fs103.mkdirSync(this.bp.localDir,{recursive:!0}),fs103.writeFileSync(this.file(),`${JSON.stringify(all,null,2)}
1439
+ ${repo}`).digest("hex").slice(0,10)}`,config2=path94.join(squadHome(),"docker-config");fs101.mkdirSync(config2,{recursive:!0});let before=words2.slice(0,up),after=words2.slice(up+1).filter(w=>w!=="-d"&&w!=="--detach");return{command:[...before,"up",...after].join(" "),stop:[...before,"stop"].join(" "),env:{COMPOSE_PROJECT_NAME:name,DOCKER_HOST:`unix://${socket}`,DOCKER_CONFIG:config2}}}function dockerGuard(guard,run6){let socket=run6.env.DOCKER_HOST.replace(/^unix:\/\//,""),under=(place,base2)=>place===base2||place.startsWith(`${base2}/`);return{...guard,noConnect:guard.noConnect.filter(s=>!under(socket,s)&&!under(s,socket)),openSocket:socket,read:[...guard.read,run6.env.DOCKER_CONFIG],write:[...guard.write,run6.env.DOCKER_CONFIG]}}import fs102 from"fs";import path95 from"path";var lockFile2=bp=>path95.join(bp.localDir,"product-lock.json"),alive=pid=>{try{return process.kill(pid,0),!0}catch(err2){return err2.code==="EPERM"}};function productHold(bp){try{let hold=JSON.parse(fs102.readFileSync(lockFile2(bp),"utf8"));if(alive(hold.pid))return hold;fs102.rmSync(lockFile2(bp),{force:!0})}catch{}return null}function takeProduct(bp,holder,kind){let now2=productHold(bp);if(now2?.holder===holder)return null;if(now2)return now2;try{return fs102.mkdirSync(bp.localDir,{recursive:!0}),fs102.writeFileSync(lockFile2(bp),JSON.stringify({holder,kind,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"}),null}catch{return productHold(bp)??{holder:"another run",kind:"story",pid:process.pid,at:new Date().toISOString()}}}function leaveProduct(bp,holder){productHold(bp)?.holder===holder&&fs102.rmSync(lockFile2(bp),{force:!0})}var WAIT_LIMIT_MS=45*6e4,LOOK_EVERY_MS=2e3;async function waitForProduct(bp,holder,kind,o={}){let until=Date.now()+(o.limitMs??WAIT_LIMIT_MS),said=!1;for(;;){let other=takeProduct(bp,holder,kind);if(!other)return;if(said||(said=!0,o.onWait?.(other)),other.wanted!==holder)try{fs102.writeFileSync(lockFile2(bp),JSON.stringify({...other,wanted:holder}))}catch{}if(o.signal?.aborted)throw new Error("You stopped the squad.");if(Date.now()>until)throw new Error(`${other.holder} kept the running product too long`);await new Promise(r=>setTimeout(r,o.lookMs??LOOK_EVERY_MS))}}var CANNOT_START="squad-kit could not start it.",APP_WAIT_MS=3*6e4,SETUP_MS=20*6e4,OUTPUT_KEEP=6e4,YOUR_APPS="Your apps";function appCopyDir(bp,app){return path96.join(bp.localDir,"apps",app.name.replace(/[^\w.-]+/g,"-")||"app")}async function updateCopy(root,repo,dir,how={}){let src=path96.join(root,repo.path),commit=latestBase(src,repo.base,repo.remote||void 0).sha;return copyTurn(dir,async()=>{if(await clearHalfCopy(src,dir,how.signal),copyIsWhole(dir))try{return await gitLater(dir,["checkout","--force","--detach",commit],how.signal?{signal:how.signal}:{}),{commit,fresh:!1}}catch(err2){if(err2 instanceof GitStopped)throw err2}return await makeCopy(src,dir,["--force","--detach"],commit,how),{commit,fresh:!0}})}var ENV_FILE2=/^\.env(\.[\w.-]+)?$/,ENV_EXAMPLE=/\.(example|sample|template|dist)$/;async function localEnvFiles(src){let found2=new Set,list2=args=>gitRecords(src,["ls-files","-z","--others","--exclude-standard",...args],"\0",f=>{let name=path96.basename(f);!f.endsWith("/")&&ENV_FILE2.test(name)&&!ENV_EXAMPLE.test(name)&&found2.add(f)}).catch(()=>{});return await list2([]),await list2(["--ignored","--directory"]),[...found2]}async function copyEnvFiles(src,dir,named=[]){let files=[...new Set([...await localEnvFiles(src),...named.filter(f=>fs103.existsSync(path96.join(src,f)))])];for(let f of files){let to=path96.join(dir,f);fs103.mkdirSync(path96.dirname(to),{recursive:!0}),fs103.copyFileSync(path96.join(src,f),to),fs103.chmodSync(to,384)}return files.sort()}var DEP_FILES=new Set(["package.json","package-lock.json","npm-shrinkwrap.json","pnpm-lock.yaml","yarn.lock","bun.lock","bun.lockb","pyproject.toml","poetry.lock","Pipfile","Pipfile.lock","uv.lock","setup.py","setup.cfg","Gemfile","Gemfile.lock","go.mod","go.sum","Cargo.toml","Cargo.lock","composer.json","composer.lock","pom.xml","build.gradle","build.gradle.kts","packages.lock.json","Directory.Packages.props","mix.exs","mix.lock","pubspec.yaml","pubspec.lock"]),DEP_NAMES=[/^requirements.*\.txt$/,/\.(cs|fs|vb)proj$/],DB_CHANGES=/(?:^|\/)(?:migrations?|migrate)\/|(?:^|\/)alembic\/versions\/|(?:^|\/)(?:schema\.prisma|schema\.rb|structure\.sql)$/i;function changesSetup(file3){let name=path96.posix.basename(file3);return DEP_FILES.has(name)||DEP_NAMES.some(p=>p.test(name))||DB_CHANGES.test(file3)}async function dependencyPrint(dir,commit){let lines3=[];return await gitRecords(dir,["ls-tree","-r",commit],`
1440
+ `,l=>{changesSetup(l.split(" ")[1]??"")&&lines3.push(l)}).catch(()=>{}),createHash9("sha1").update(lines3.join(`
1441
+ `)).digest("hex").slice(0,12)}async function setupPrintNow(dir){let files=new Set;await gitRecords(dir,["ls-files","-z","--cached","--others","--exclude-standard"],"\0",f=>{f&&changesSetup(f)&&files.add(f)}).catch(()=>{});let hash2=createHash9("sha1");for(let f of[...files].sort()){hash2.update(`${f}\0`);try{hash2.update(fs103.readFileSync(path96.join(dir,f)))}catch{hash2.update("gone")}hash2.update("\0")}return hash2.digest("hex").slice(0,12)}function onPort(command,from,to){if(/\$\{?PORT\b|%PORT%/.test(command))return command;let n=String(from),settings=[new RegExp(`(--port[= ])${n}\\b`),new RegExp(`(\\s-p ?)${n}\\b`),new RegExp(`(\\bPORT=)${n}\\b`),new RegExp(`((?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1?\\]):)${n}\\b`)];for(let s of settings)if(s.test(command))return command.replace(s,`$1${to}`);return null}function portOf2(url){let u=new URL(url);return Number(u.port)||(u.protocol==="https:"?443:80)}function withPort(url,port){let u=new URL(url);return u.port=String(port),url.endsWith("/")?u.toString():u.toString().replace(/\/$/,"")}var LOCAL2=new Set(["localhost","127.0.0.1","0.0.0.0","[::1]","[::]"]);function isLocal(url){try{return LOCAL2.has(new URL(url).hostname)}catch{return!1}}function connects(host,port){return new Promise(resolve2=>{let s=net3.connect({host,port}),done=yes=>{s.destroy(),resolve2(yes)};s.setTimeout(800,()=>setImmediate(()=>done(!1))),s.once("connect",()=>done(!0)),s.once("error",()=>done(!1))})}async function portTaken(port){return await connects("127.0.0.1",port)||await connects("::1",port)}async function nextFreePort(from){for(let p=from+1;p<from+50&&p<65536;p+=1)if(!await portTaken(p))return p}function whoListens(port){if(process.platform==="win32")return null;try{let out18=execFileSync12("lsof",["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-Fpc"],{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:3e3}),pid=Number(/^p(\d+)/m.exec(out18)?.[1]);return pid?{pid,program:/^c(.+)$/m.exec(out18)?.[1]??""}:null}catch{return null}}function psField(pid,field){if(process.platform==="win32")return null;try{return Number(execFileSync12("ps",["-o",`${field}=`,"-p",String(pid)],{encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:3e3}).trim())||null}catch{return null}}var sleep3=ms2=>new Promise(r=>setTimeout(r,ms2));function addressIn(output){return/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\]):\d{2,5}[^\s'"]*/i.exec(output.replace(/\x1b\[[0-9;]*m/g,""))?.[0].replace("0.0.0.0","127.0.0.1").replace(/\/$/,"")}function busyIn(output){let m=/(?:EADDRINUSE|address already in use)[^\n]*?:(\d{2,5})\b|port (\d{2,5}) is (?:already )?in use/i.exec(output);return m?Number(m[1]??m[2]):void 0}function squadKitOn(bp,port){let holder=whoListens(port);if(!holder)return null;let group=psField(holder.pid,"pgid")??holder.pid,records={};try{records=JSON.parse(fs103.readFileSync(path96.join(bp.localDir,"apps.json"),"utf8"))}catch{}let repo=Object.entries(records).find(([,r])=>r.pid===group)?.[0];if(repo!==void 0){let app=projectApps(readMeta(bp)).find(a=>a.repo===repo);return{group,holder,...app?{app:app.name}:{}}}return psField(group,"ppid")===process.pid?{group,holder}:null}var AppError=class extends Error{},AppRunner=class{constructor(bp,opts={}){this.bp=bp;this.opts=opts}bp;opts;runs=new Map;output=new Map;guardFor(cwd){if(!guardSupport().on)return;let allow=readMeta(this.bp).guard;return projectGuardFor(this.bp,{cwd,...allow?{allow}:{}})}find(ref2){try{return findApp(readMeta(this.bp),ref2)}catch(err2){throw new AppError(err2.message)}}repoOf(app){return reposOf(readMeta(this.bp)).find(r=>r.path===app.repo)}file(){return path96.join(this.bp.localDir,"apps.json")}records(){try{return JSON.parse(fs103.readFileSync(this.file(),"utf8"))}catch{return{}}}record(repo,change){let all=this.records(),r=all[repo]??{};change(r),all[repo]=r,fs103.mkdirSync(this.bp.localDir,{recursive:!0}),fs103.writeFileSync(this.file(),`${JSON.stringify(all,null,2)}
1441
1442
  `,"utf8")}elsewhere(repo){let r=this.records()[repo];return r?.pid&&groupAlive(r.pid)&&!takenOver(r.pid,r.startedAt)&&!this.runs.get(repo)?.child?r:null}oursOn(port){return squadKitOn(this.bp,port)}keep(repo,text3){this.output.set(repo,((this.output.get(repo)??"")+text3).slice(-OUTPUT_KEEP)),this.opts.echo?.(text3)}async status(){return Promise.all(projectApps(readMeta(this.bp)).map(a=>this.view(a)))}async statusOf(ref2){return this.view(this.find(ref2))}async view(app){let run6=this.runs.get(app.repo),dir=appCopyDir(this.bp,app),base2={...app,dir};if(run6){let at3=run6.address??app.url;return{...base2,state:run6.state,...run6.state==="starting"&&run6.step?{step:run6.step}:{},...run6.address&&run6.state!=="failed"?{address:run6.address}:{},startedAt:run6.startedAt,...run6.since&&run6.state==="running"?{since:run6.since}:{},...run6.error?{error:run6.error}:{},...run6.detail?{detail:run6.detail}:{},...run6.busy?{busy:run6.busy}:{},...run6.commit?{commit:run6.commit}:{},up:at3?await isUp(at3):!1}}let away=this.elsewhere(app.repo);if(away)return{...base2,state:"running",address:away.url,startedAt:away.startedAt,since:away.startedAt,commit:away.commit,elsewhere:!0,up:away.url?await isUp(away.url):!1};let last=this.records()[app.repo]?.startedAt;return{...base2,state:"not-running",...last?{startedAt:last}:{},up:app.url?await isUp(app.url):!1}}lines(ref2,count3=200){let app=this.find(ref2);return(this.output.get(app.repo)??"").replace(/\x1b\[[0-9;]*m/g,"").split(`
1442
1443
  `).slice(-count3)}notNow(ref2,ticket){let app=this.find(ref2);this.record(app.repo,r=>r.notNow=[...new Set([...r.notNow??[],ticket])].slice(-50))}notNowOf(repo){return this.records()[repo]?.notNow??[]}start(ref2,opts={}){let app=this.find(ref2);if(!app.start)throw new AppError(`squad-kit does not know how to start ${app.name} yet. Ask the Architect to find it, or type the start command.`);let now2=this.runs.get(app.repo);if(now2?.state==="starting")throw new AppError(`${app.name} is starting already.`);if(now2?.state==="running")throw new AppError(`${app.name} is already running at ${now2.address??app.url}.`);let away=this.elsewhere(app.repo);if(away&&!opts.stopOther)throw new AppError(`${app.name} is already running at ${away.url??app.url}, started outside the board. Stop it first, or press Restart.`);let stop=new AbortController,settle,exit,run6={state:"starting",step:"update",startedAt:new Date().toISOString(),stop,settled:new Promise(r=>settle=r),exited:new Promise(r=>exit=r)};return this.runs.set(app.repo,run6),this.output.set(app.repo,""),this.launch(app,run6,opts,exit).catch(err2=>this.fail(run6,CANNOT_START,{detail:err2.message})).finally(()=>{run6.child||exit(),settle()}),{...app,dir:appCopyDir(this.bp,app),state:"starting",step:"update",startedAt:run6.startedAt,up:!1}}async settled(ref2){let app=this.find(ref2);return await this.runs.get(app.repo)?.settled,this.view(app)}async exited(ref2){await this.runs.get(this.find(ref2).repo)?.exited}fail(run6,error2,more3={}){run6.stop.signal.aborted||(run6.state="failed",run6.error=error2,more3.detail&&(run6.detail=more3.detail),more3.busy&&(run6.busy=more3.busy),this.leaveIfIdle())}leaveIfIdle(){[...this.runs.values()].some(r=>r.state==="starting"||r.state==="running")||leaveProduct(this.bp,YOUR_APPS)}needsOf(app,opts){let meta=readMeta(this.bp);if(!runsAsProduct(meta))return[];let parts2=partsOf(meta),repos=(parts2.find(p=>p.repo===app.repo&&!p.shared)?.needs??[]).flatMap(id2=>{let p=parts2.find(x=>x.id===id2);return p?.repo&&!p.shared&&!p.off&&!p.noRun&&p.repo!==app.repo&&!opts.asked?.includes(p.repo)?[p.repo]:[]});return projectApps(meta).filter(a=>a.start&&repos.includes(a.repo))}async needReady(need,app,opts){let now2=this.runs.get(need.repo);return now2?.state==="running"||this.elsewhere(need.repo)||now2?.state!=="starting"&&need.url&&await isUp(need.url)?!0:(now2?.state!=="starting"&&this.start(need.name,{...opts,asked:[...opts.asked??[],app.repo]}),(await this.settled(need.name)).state==="running")}async launch(app,run6,opts,exited){let repo=this.repoOf(app),dir=appCopyDir(this.bp,app),{signal:signal2}=run6.stop,fixed=runsAsProduct(readMeta(this.bp));if(fixed){let other=takeProduct(this.bp,YOUR_APPS,"apps");if(other)return this.fail(run6,`The squad is running the product for ${other.holder.replace(/^try:/,"")} right now, on the same addresses. Start this again when that is done.`)}for(let need of this.needsOf(app,opts)){if(signal2.aborted)return;if(!await this.needReady(need,app,opts))return this.fail(run6,`${need.name} did not start, so ${app.name} cannot work yet. See what ${need.name} says.`)}if(signal2.aborted||(await this.elsewhereStopped(app,opts),signal2.aborted))return;let copy2;try{copy2=await updateCopy(this.bp.root,repo,dir,{signal:signal2,making:()=>run6.step="copy"})}catch(err2){return err2 instanceof NoRoomForCopy?this.fail(run6,err2.message):this.fail(run6,`squad-kit could not get the newest ${repo.base} ready.`,{detail:err2.message})}if(run6.commit=copy2.commit,signal2.aborted)return;let named=(readMeta(this.bp).localFiles??[]).filter(f=>f.repo===repo.path).map(f=>f.path);try{let env3=await copyEnvFiles(path96.join(this.bp.root,repo.path),dir,named);env3.length&&this.keep(app.repo,`Copied from your project folder: ${env3.join(", ")}
1443
1444
  `)}catch(err2){return this.fail(run6,"squad-kit could not copy this project's settings into its own copy.",{detail:`Could not copy your .env files: ${err2.message}`})}if(signal2.aborted)return;let url=app.url,command=app.start,moved={},port=url&&isLocal(url)?portOf2(url):null;if(port&&await portTaken(port)){let ours=opts.stopOther?this.oursOn(port):null;if(!(ours?await this.freePort(app,port,ours):!1)){let own2=portSettings(dir,port),next=!fixed&&(onPort(command,port,port+1)!==null||own2.reads)?await nextFreePort(port):void 0;if(!next)return this.fail(run6,...await this.busyPort(app,port));command=onPort(command,port,next)??command,url=withPort(url,next),moved=Object.fromEntries(own2.vars.map(name=>[name,String(next)])),this.keep(app.repo,`Port ${port} is used by another program, so ${app.name} runs on port ${next}${own2.vars.length?` (set as PORT and ${own2.vars.join(", ")})`:""}
@@ -1546,7 +1547,7 @@ Then carry on and hand in your answer again${found2.q.at==="question"?" without
1546
1547
  ${resultText}`,cwd)]})}}return{sessionId:id2,...cwd?{cwd}:{},updates:updates.slice(-limit),total:updates.length}}function cwdOf(id2,base2){let file3=sessionFile2(id2,base2);return file3?fs112.readFileSync(file3,"utf8").slice(0,2e4).match(/"cwd":"((?:[^"\\]|\\.)*)"/)?.[1]?.replace(/\\(.)/g,"$1"):void 0}function ticketConversations(t,base2,nativeDir){return CONVERSATIONS.flatMap(c=>{let id2=t.sessions?.[c.key];if(!id2)return[];let started=!!sessionFile2(id2,base2)||!!(nativeDir&&fs112.existsSync(path105.join(nativeDir,`${id2}.jsonl`)));return[{key:c.key,label:c.label,sessionId:id2,started}]})}function ticketFolders(t,bp){return[t.storyDir??t.worktree,bp.proofDir,ticketDir(bp,t.id),ticketMediaDir(bp,t.id)].filter(d=>!!d)}function allowedFile(file3,folders){try{let real2=fs112.realpathSync(file3);return folders.some(f=>{try{let root=fs112.realpathSync(f);return real2===root||real2.startsWith(root+path105.sep)}catch{return!1}})&&fs112.statSync(real2).isFile()?real2:null}catch{return null}}function chatSummaries(tickets,q4=""){let needle=q4.trim().toLowerCase(),out18=[];for(let t of tickets){let c=t.chat;if(!c||!c.open&&!c.messages.length)continue;let last=c.messages.at(-1),mine2=c.messages.map(m=>m.who).lastIndexOf("you"),fresh=c.messages.slice(mine2+1).filter(m=>m.who!=="you"&&m.who!=="squad").length,match;if(needle&&!`${t.id} ${t.title}`.toLowerCase().includes(needle)){let hit=c.messages.find(m=>m.text.toLowerCase().includes(needle));if(!hit)continue;let i=hit.text.toLowerCase().indexOf(needle);match=`${i>40?"\u2026":""}${hit.text.slice(Math.max(0,i-40),i+needle.length+60).replace(/\s+/g," ").trim()}`}out18.push({id:t.id,title:t.title,stage:t.stage,...t.release?{release:t.release}:{},with:c.with,open:c.open,thinking:!!c.thinking,beside:!!c.beside,yourTurn:c.open&&!c.thinking&&!!last&&last.who!=="you",fresh,count:c.messages.length,at:last?.at??t.activity[0]?.at??t.createdAt,...last?{last:{who:last.who,text:last.text.slice(0,240),at:last.at}}:{},...match?{match}:{}})}return out18.sort((a,b)=>Number(b.open)-Number(a.open)||b.at.localeCompare(a.at))}var QUALITY_NAME={best:"Best",balanced:"Balanced",thrifty:"Thrifty"},CapReached=class extends Error{constructor(role,step2,by2,caps,quality,who4,run6){super(capWords(who4,by2,caps,quality));this.role=role;this.step=step2;this.by=by2;this.caps=caps;this.quality=quality;this.who=who4;this.run=run6;this.name="CapReached"}role;step;by;caps;quality;who;run};function capWords(who4,by2,caps,quality){let what=by2==="turns"?`used the ${caps.turns} turns one step may take`:`reached the $${caps.spendUsd} one step may spend`;return`The ${who4} ${what} on ${QUALITY_NAME[quality]}, so squad-kit paused it. Its work so far is kept.`}function capCard(cap4){return{gate:"cap",question:cap4.message,options:[{do:"more",name:"Give it more",note:"It carries on where it stopped, with twice the turns and spend for this step."},{do:"stop",name:"Stop",note:"Leave it here. Nothing is lost: you can carry on later, fix it yourself or send it back to the plan."}]}}import fs113 from"fs";import path106 from"path";var ProductError=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail};function mainCopyDir(bp,repo){return path106.join(productCopiesDir(bp),repo.replace(/[^\w.-]+/g,"-"))}var productCopiesDir=bp=>path106.join(bp.localDir,"product");function changedParts(meta,t){if(t.parts?.length)return t.parts;let repo=repoFor(meta,t).path;return partsOf(meta).filter(p=>p.repo===repo).map(p=>p.id)}function productPlan(bp,meta,t){let copies=copiesOf(meta,t),parts2=partsToRun(meta,changedParts(meta,t)),places=[];for(let part of parts2){let repo=reposOf(meta).find(r=>r.path===part.repo);if(!repo||places.some(p=>p.repo.path===repo.path))continue;let inRepo=parts2.filter(p=>p.repo===repo.path),front=inRepo.find(p=>!inRepo.some(x=>x.needs.includes(p.id)))??part,start=repo.commands.start?.trim(),url=(repo.commands.url??front.url)?.trim();if(!start)return{places:[],why:`squad-kit does not know how to start ${front.name} yet. An engineer can set it in Settings.`};if(!url)return{places:[],why:`squad-kit does not know the address of ${front.name} yet. An engineer can set it in Settings.`};let copy2=copies.find(c=>c.repo.path===repo.path);places.push({part:front,repo,dir:copy2?.dir??mainCopyDir(bp,repo.path),start,url,fromStory:!!copy2})}let needed=new Set(places.flatMap(p=>p.part.needs)),tops=places.filter(p=>!needed.has(p.part.id)),changed=changedParts(meta,t);return{places,...places.length?{front:tops.find(p=>changed.includes(p.part.id))??tops[0]??places.at(-1)}:{}}}var guardOf2=(bp,t,dir)=>{if(!guardSupport().on)return;let allow=readMeta(bp).guard;return ticketGuard(bp,{cwd:dir,id:t.id,...allow?{allow}:{}})};async function readyMainCopy(bp,t,place,signal2){let{dir,repo}=place,{commit,fresh}=await updateCopy(bp.root,repo,dir,signal2?{signal:signal2}:{});await fillOwnCopy(bp,repo.path,dir);let setup=repo.commands.setup?.trim();if(!setup)return;let print=await dependencyPrint(dir,commit),mark=`${dir}.setup`;if(!fresh&&fs113.existsSync(mark)&&fs113.readFileSync(mark,"utf8")===print)return;let guard=guardOf2(bp,t,dir),r=await runCommand(dir,setup,SETUP_MS,{silentMs:CHECK_SILENT_MS,...signal2?{signal:signal2}:{},...guard?{guard}:{}});if(!r.ok)throw new ProductError(dockerBlockedWords(r.output,guard)??`squad-kit could not set up ${place.part.name}.`,`${setup}
1547
1548
  ${tail(r.output,30)}`);await fillOwnCopy(bp,repo.path,dir),fs113.writeFileSync(mark,print)}var running2=new Map,keyOf3=(bp,holder)=>`${bp.root}
1548
1549
  ${holder}`;function stopProduct(bp,holder){let key=keyOf3(bp,holder);for(let{app}of running2.get(key)??[])app.stop();running2.delete(key),leaveProduct(bp,holder)}async function startProduct(bp,t,holder,kind,o={}){let plan=productPlan(bp,readMeta(bp),t);if(plan.why)throw new ProductError(plan.why);await waitForProduct(bp,holder,kind,{...o.signal?{signal:o.signal}:{},...o.onWait?{onWait:o.onWait}:{},...o.limitMs?{limitMs:o.limitMs}:{},...o.lookMs?{lookMs:o.lookMs}:{}});let key=keyOf3(bp,holder),list2=running2.get(key)??[];running2.set(key,list2);try{let front;for(let place of plan.places){place.fromStory||await readyMainCopy(bp,t,place,o.signal);let was=list2.find(r=>r.repo===place.repo.path);was&&was.app.ended?.()&&list2.splice(list2.indexOf(was),1);let app=was&&!was.app.ended?.()?was.app:void 0;app||(o.log?.(`Starting ${place.part.name} (${place.fromStory?"the story's copy":"main"}) at ${place.url}: ${place.start}`),app=await startPart(bp,t,place,o),list2.push({app,repo:place.repo.path})),place===plan.front&&(front=app)}return front}catch(err2){throw stopProduct(bp,holder),err2}}var composeStops=new Map;async function startPart(bp,t,place,o){await composeStops.get(place.url);let guard=guardOf2(bp,t,place.dir),compose=composeRun(readMeta(bp),place.repo.path,place.start,place.dir,bp.root);if(compose&&"why"in compose)throw new ProductError(compose.why);let own2=compose&&guard?dockerGuard(guard,compose):guard;try{let app=await startApp(place.dir,compose?.command??place.start,place.url,o.waitMs??(compose?DOCKER_WAIT_MS:void 0),o.signal,own2,{fixed:!0,...compose?{env:compose.env}:{}});return compose?{...app,stop:()=>{app.stop();let stopped=runCommand(place.dir,compose.stop,DOCKER_STOP_MS,{...own2?{guard:own2}:{},env:compose.env}).then(()=>{},()=>{});composeStops.set(place.url,stopped),stopped.then(()=>composeStops.delete(place.url))}}:app}catch(err2){throw productFailure(err2,place,own2)}}function productFailure(err2,place,guard){let message=err2.message;return err2 instanceof ProductError?err2:err2 instanceof AddressTaken?new ProductError(`Another program on this computer is using the address of ${place.part.name}, so squad-kit could not run the product. Close that program, then press Try again.`,message):new ProductError(dockerBlockedWords(message,guard)??`squad-kit could not start ${place.part.name}.`,message)}function waitWords2(hold){if(hold.kind==="apps")return"Waiting for Your apps: they use the same addresses. Stop them, and this goes on.";let who4=hold.holder.replace(/^try:/,"");return hold.kind==="try"?`Waiting for you to finish trying ${who4}: it uses the same addresses.`:`Waiting for ${who4}: only one story at a time runs the product.`}function answerProblem(schema,value){return value==null?"there is no answer":problem(schema,value,"")}function problem(shape2,value,at3){let name=at3||"the answer";if(shape2.type&&!fits2(shape2.type,value))return`${name} is not ${KIND[shape2.type]??shape2.type}`;if(shape2.enum&&!shape2.enum.includes(value))return`${name} is not one of ${shape2.enum.join(", ")}`;if(Array.isArray(value)){if(shape2.minItems!==void 0&&value.length<shape2.minItems)return`${name} has fewer than ${shape2.minItems} items`;if(shape2.maxItems!==void 0&&value.length>shape2.maxItems)return`${name} has more than ${shape2.maxItems} items`;if(shape2.items)for(let[i,item]of value.entries()){let p=problem(shape2.items,item,`${at3||"item"}[${i}]`);if(p)return p}}else if(typeof value=="object"&&value!==null){let o=value;for(let key of shape2.required??[])if(o[key]===void 0||o[key]===null)return`${at3?`${at3}.`:""}${key} is missing`;for(let[key,sub]of Object.entries(shape2.properties??{})){if(o[key]===void 0||o[key]===null)continue;let p=problem(sub,o[key],at3?`${at3}.${key}`:key);if(p)return p}}return null}var KIND={object:"an object",array:"a list",string:"text",boolean:"true or false",integer:"a whole number",number:"a number",null:"empty"};function fits2(type,v){switch(type){case"object":return typeof v=="object"&&v!==null&&!Array.isArray(v);case"array":return Array.isArray(v);case"string":return typeof v=="string";case"boolean":return typeof v=="boolean";case"integer":return Number.isInteger(v);case"number":return typeof v=="number"&&Number.isFinite(v);case"null":return v===null;default:return!0}}function dependsProblem(value,open3,self){let list2=value?.dependsOn;if(list2==null)return null;if(!Array.isArray(list2))return"dependsOn is not a list";let known=new Set(open3.map(x=>x.toUpperCase()));for(let item of list2){if(typeof item!="string")return"dependsOn has an item that is not text";let name=item.trim().toUpperCase();if(name===self.toUpperCase())return`dependsOn names ${self}, this ticket itself`;if(!known.has(name))return`dependsOn names ${item.trim()||"an empty name"}, which is not an open ticket on this board${open3.length?` (open: ${open3.join(", ")})`:" (there are none)"}`}return null}var OVERVIEW_PARTS=["users","can","worth","ideas","parts","links"];function scanProblem(value){if(!value||typeof value!="object")return null;let v=value;if(v.empty!==!0){for(let key of OVERVIEW_PARTS)if(v[key]===void 0||v[key]===null)return`${key} is missing`}return partsProblem(value)}function partsProblem(value){if(!value||typeof value!="object")return null;let v=value,ids=(Array.isArray(v.parts)?v.parts:[]).map(p=>p&&typeof p=="object"?p.id:void 0),twice=ids.find((id2,i)=>ids.indexOf(id2)!==i);if(twice!==void 0)return`two parts have the id "${String(twice)}"`;let known=new Set(ids);for(let[i,l]of(Array.isArray(v.links)?v.links:[]).entries()){let link=l&&typeof l=="object"?l:{};for(let end of["from","to"])if(!known.has(link[end]))return`links[${i}].${end} names no part ("${String(link[end])}")`}return null}function portHolder(p){return p.app?`your app ${p.app}`:"a program squad-kit started earlier"}function portWords(p){return p.ours?`${upperFirst(portHolder(p))} is in the way, so the app does not answer.`:"Another program on this computer is in the way, so the app does not answer. squad-kit leaves it alone. Close that program, then press Stop and try again."}function portDetail(p){return`${p.program||"A program"}${p.pid?` (process ${p.pid})`:""} listens on port ${p.port}, the port in the app's address${p.app?`: your app ${p.app}, which squad-kit runs from Your apps`:p.ours?", and squad-kit started it":""}.`}function portParts(q4){return q4.port?{fact:{label:"What is in the way",value:portWords(q4.port)},...q4.port.ours?{freePort:{port:q4.port.port,what:portHolder(q4.port)}}:{}}:{}}function slowDetail(q4){let lines3=[q4.command?`Waiting on: ${q4.command}${q4.pid?` (process ${q4.pid})`:""}`:"",q4.port?portDetail(q4.port):""].filter(Boolean);return lines3.length?lines3.join(`
1549
- `):void 0}var SAVING="Saving the change, then it merges",RESUMED="resumed after a restart",QUEUED2="Next in line";function queuedLine(busy,limit){return`${QUEUED2}: starts when a slot is free (${busy} of ${limit} busy)`}var TRACK=["tickets","specs","implement","test","review","release"],YOUR_PART={questions:"answer the questions",spec:"approve the spec",plan:"approve the design",proof:"check the proof",stuck:"pick a way out",release:"release it",ask:"answer the question",yours:"give it back",chat:"finish the talk",change:"decide on the design update",depends:"choose how to build it"};function lowerFirst2(s){return/^[A-Z][A-Z0-9]+\b/.test(s)?s:s.charAt(0).toLowerCase()+s.slice(1)}function upperFirst(s){return s.charAt(0).toUpperCase()+s.slice(1)}function shortDoing(line3){let s=line3.replace(/(?:[\w.@~-]*\/)+([\w.@-]+)/g,"$1").replace(/\s+/g," ").trim();return lowerFirst2(s.length>56?`${s.slice(0,55)}\u2026`:s)}function stateOf(t){return t.stage==="done"?"done":t.waiting||t.takenOver?"you":t.stopRequested?"stopping":t.status.startsWith(QUEUED2)?"queued":t.stage==="release"?"ready":t.squad||t.live?"working":"idle"}function guessClock(t){let s=t.status,doing=t.steps?.findIndex(x=>x.status==="doing")??-1;switch(t.stage){case"tickets":case"bugs":return{who:"analyst",step:"questions"};case"specs":return/^Analyst/.test(s)||t.track!=="express"&&!t.specApproved?{who:"analyst",step:"spec"}:{who:"architect",step:"design"};case"implement":return doing>=0?{who:"builder",step:"build",part:doing+1}:{who:"builder",step:/fix/i.test(s)?"fix":"build"};case"test":return/Tester|screen|screenshot/i.test(s)?{who:"tester",step:"proof"}:/Builder/.test(s)?{who:"builder",step:"fix"}:{who:"squad",step:"checks"};default:return/Builder/.test(s)?{who:"builder",step:"fix"}:/Tester|proof|screen/i.test(s)?{who:"tester",step:"proof"}:{who:"reviewer",step:"review"}}}var SENTENCE={questions:()=>"The Analyst is reading the ticket to see what it needs to ask you.",spec:t=>t.questions?.some(q4=>q4.answer)?"The Analyst is writing the spec from your answers.":'The Analyst is writing the spec: the story and the "done when" list.',design:t=>t.track==="express"?"The Architect is writing the plan.":t.track==="auto"?"The Architect is reading the part of the BRD and the code, and writing the design.":"The Architect is choosing how to build it and writing the design.",build:()=>"The Builder is writing the code.",fix:()=>"The Builder is fixing what the checks or the Reviewer found.",checks:()=>"squad-kit is running the project's own checks, so the squad never grades its own work.",review:()=>"The Reviewer, a fresh agent, is reading the changes.",proof:()=>'The Tester is opening the app and taking a screenshot for each "done when".'};function screens(t){return changesScreens(t)}var MERGES_BY_ITSELF="nothing: it is accepted and merged by itself";function ahead(t,step2,part){let after=["review",...screens(t)?["proof"]:[]],proof={what:t.track==="auto"?MERGES_BY_ITSELF:"check the proof"};switch(step2){case"questions":return{steps:[],builds:0,what:"answer its questions, or approve the spec"};case"spec":return{steps:[],builds:0,what:"approve the spec"};case"design":return t.track!=="express"&&t.track!=="auto"?{steps:[],builds:0,what:"approve the design"}:{steps:["checks",...after],builds:t.work?.steps??t.steps?.length??0,...proof};case"build":return{steps:["checks",...after],builds:part?part.of-part.n:0,...proof};case"fix":return{steps:["checks",...after],builds:0,...proof};case"checks":return{steps:after,builds:0,...proof};case"review":return{steps:screens(t)?["proof"]:[],builds:0,...proof};case"proof":return{steps:[],builds:0,...proof}}}function nextWords(t,step2,part){let shots=screens(t)?', then the Tester takes a screenshot for each "done when"':"",ready=t.track==="auto"?"The squad accepts it by itself and merges it.":"The proof is ready for you to check.";switch(step2){case"questions":return"The Analyst asks you what it needs to know, or writes the spec straight away.";case"spec":return"You read the spec and approve it. Then the Architect writes the design.";case"design":return t.track==="express"?"Express track: the plan is approved on its own, then the Builder starts and the checks and the Reviewer follow.":t.track==="auto"?"Builds by itself: the plan is approved on its own, then the Builder starts and the checks and the Reviewer follow.":"You read the design and approve it. Then the Builder starts.";case"build":return part&&part.n<part.of?`The Builder goes on with step ${part.n+1} of ${part.of}. Then the checks run on their own, a fresh Reviewer reads the work${shots}.`:`The checks run on their own, a fresh Reviewer reads the work${shots}.`;case"fix":return`The checks run again on their own, then the Reviewer reads the work${shots}.`;case"checks":return`The Reviewer, a fresh agent, reads the changes${shots}.`;case"review":return screens(t)?'The Tester takes a screenshot for each "done when".':ready;case"proof":return ready}}function inAbout(seconds2){let m=Math.round(seconds2/60);return m<=1?"in about a minute":`in about ${m} minutes`}function barOf(t,step2,part){if(part)return Array.from({length:part.of},(_,i)=>i+1<part.n?"done":i+1===part.n?"now":"todo");let group=["questions","spec","design"].includes(step2)?t.track==="express"||t.track==="auto"?["design"]:["questions","spec","design"]:["checks","review",...screens(t)?["proof"]:[]],at3=group.indexOf(step2==="fix"||step2==="build"?"checks":step2);return group.map((_,i)=>i<at3?"done":i===at3?"now":"todo")}var STEP_STAGE={questions:"tickets",spec:"specs",design:"specs",build:"implement",fix:"implement",checks:"test",review:"review",proof:"review"},LONG_WAIT_S=600,trackStageOf=s=>s==="bugs"?"tickets":s;function runsIn(t,stage){return(t.history??[]).filter(r=>trackStageOf(r.stage??STEP_STAGE[r.step])===stage)}function workIn(t,stage,at3){let runs2=runsIn(t,stage),done=runs2.reduce((n,r)=>{let secs=(Date.parse(r.to)-Date.parse(r.from))/1e3;return n+(Number.isFinite(secs)?Math.max(0,secs):0)},0),running3=t.clock&&trackStageOf(t.stage)===stage?runSeconds(t.clock,at3):void 0;return running3!==void 0?done+running3:runs2.length?done:t.history?.length&&t.history.length<HISTORY?0:void 0}function builderRunWords(t){let b=runsIn(t,"test").filter(r=>r.step==="checks").pop()?.builderRun;return b?.checks.length?b.all?" \xB7 checked with the Builder's run":` \xB7 ${b.checks.map(c=>c==="test"?"tests":c==="lint"?"code check":c).join(" and ")} from the Builder's run`:""}function waitWords3(t,stage,work){let spent=t.stageTimes?.spent[stage];if(spent===void 0)return;let wait=Math.max(0,spent-work),yours=Math.min(wait,t.stageTimes?.yours?.[stage]??0);return yours>=LONG_WAIT_S?`waited ${durationWords(yours)} for you`:wait>=LONG_WAIT_S?`waited ${durationWords(wait)}`:void 0}function stageNotes(t,state,book,at3){let stage=trackStageOf(t.stage),cur=stage==="done"?TRACK.length:TRACK.indexOf(stage),st=t.stageTimes,usual=(who4,step2)=>middle(usualOf(book,who4,step2,t.track)),about=s=>s>0?`about ${Math.max(1,Math.round(s/60))} min`:"";return TRACK.map((s,i)=>{let note2="",wait;if(i<cur){let work=workIn(t,s,at3),yours=t.track!=="express"&&t.track!=="auto"&&t.specApproved,said=secs=>`${durationWords(secs)}${s==="specs"&&yours?", you approved":""}`;if(work===void 0){let spent=st?.spent[s];note2=spent!==void 0?said(spent):""}else runsIn(t,s).length&&(note2=`${said(work)}${s==="test"?builderRunWords(t):""}`,wait=waitWords3(t,s,work))}else if(i===cur){let inStage=(t.clock||runsIn(t,s).length?workIn(t,s,at3):void 0)??(st?.stage===t.stage?(at3-Date.parse(st.since))/1e3:void 0);state==="you"?note2="your turn":state==="ready"?note2=t.release?`in ${t.release}`:"accepted":state==="queued"?note2="next in line":(state==="working"||state==="stopping")&&(note2=inStage!==void 0?`now \xB7 ${durationWords(inStage)}${s==="test"?builderRunWords(t):""}`:"now")}else s==="review"?note2=t.track==="auto"?"accepted by itself":"you check proof":s==="release"?note2=t.track==="auto"?"merges by itself":"on Looks right":s==="specs"?note2=about(t.track==="express"||t.track==="auto"?usual("architect","design"):usual("analyst","spec")+usual("architect","design")):s==="implement"?note2=about((t.work?.steps??t.steps?.length??0)*usual("builder","build")):s==="test"&&(note2=about(usual("squad","checks")));return{stage:s,note:note2,...wait?{wait}:{}}})}function noteWhenWords(t,now2){switch(now2.state){case"working":{let left=now2.usual?Math.max(60,middle(now2.usual)-(now2.seconds??0)):60;return now2.role==="squad"?`squad-kit is running the checks. The next agent reads it when its step starts, ${inAbout(left)}.`:`The ${now2.who} reads it when its next step starts, ${inAbout(left)}.`}case"you":return`No agent is running now, so nobody reads it yet. The squad reads it when it carries on, after you ${t.waiting?YOUR_PART[t.waiting.kind]:"give it back"}.`;case"queued":return"No agent is running now, so nobody reads it yet. It is next in line: the squad reads it when a slot is free and its step starts.";case"stopping":return"The squad is stopping. It reads it when the work carries on.";case"ready":case"done":return"The work is accepted, so no agent works on it any more. The note stays on the ticket for the record.";default:return"No agent is running now, so nobody reads it yet. The squad reads it when work on the ticket starts."}}function slowCard(t,c,part,lastAt,at3){let q4=t.quiet;if(!q4)return;let who4=WHO[c.who],where=`${t.id} \xB7 ${part?`step ${part.n} of ${part.of}`:who4}`,limit=c.limit?`If you do nothing, squad-kit stops this step at ${c.limit} minutes. All work is kept.`:void 0,port=portParts(q4),detail=slowDetail(q4);if(q4.kind==="stuck"){let quiet=Math.max(0,(at3-Date.parse(q4.since))/1e3);return{kind:"stuck",title:"Looks stuck",where,text:`No progress for ${minutesWords(quiet)}.${c.doing?` ${c.who==="squad"?"squad-kit":`The ${who4}`} was ${lowerFirst2(c.doing)}.`:""}`,facts:[{label:"What we checked",value:q4.detail},...port.fact?[port.fact]:[],{label:"Your work",value:"Safe. Everything done so far is kept."}],...limit?{limit}:{},canEnd:!!q4.pid,...port.freePort?{freePort:port.freePort}:{},...detail?{detail}:{}}}let facts=[];if(c.doing&&facts.push({label:"Doing now",value:upperFirst(c.doing)}),lastAt){let ago3=Math.max(0,(at3-Date.parse(lastAt))/1e3);facts.push({label:"Last progress",value:ago3<180?`${minutesWords(ago3)} ago, so it is still working`:`${minutesWords(ago3)} ago`})}return(t.tries??0)>0?facts.push({label:"Why it may be slow",value:`The checks failed ${t.tries===1?"once":`${t.tries} times`}, so the Builder is fixing them (try ${(t.tries??0)+1} of 3).`}):(t.reviewRound??0)>1&&facts.push({label:"Why it may be slow",value:`The Reviewer asked for changes, so this is round ${t.reviewRound} of 2.`}),port.fact&&facts.push(port.fact),{kind:"long",title:"Taking longer than usual",where,text:q4.detail,facts,...limit?{limit}:{},canEnd:!!q4.pid,...port.freePort?{freePort:port.freePort}:{},...detail?{detail}:{}}}function liveNow(t,book,at3=Date.now(),saving=!1){let state=stateOf(t),stages=stageNotes(t,state,book,at3),base2={state,stages};if(state==="done")return{...base2,line:t.status||"Released",sentence:`It is released${t.release?` in ${t.release}`:""}.`,noteWhen:noteWhenWords(t,{state})};if(saving||state==="ready"&&(t.squad||t.live)){let cur=trackStageOf(t.stage);return{...base2,stages:saving?stages.map(s=>s.stage===cur?{...s,note:"saving now"}:s):stages,merging:!0,line:saving?SAVING:t.status,sentence:saving?"You said Looks right. squad-kit is saving the change and merging it now. Nothing is needed from you.":"It is accepted, and squad-kit is merging it now. Nothing is needed from you.",noteWhen:noteWhenWords(t,{state:"ready"})}}if(state==="ready")return{...base2,line:t.status,sentence:`It is accepted and goes out with ${t.release??"the next release"}. Nothing is needed from you until then.`,noteWhen:noteWhenWords(t,{state})};if(state==="you"){let summary=t.waiting?.summary??"You took it over. Give it back when you are done";return{...base2,line:t.status,sentence:`It waits for you: ${lowerFirst2(summary)}.`,next:`The squad carries on after you ${t.waiting?YOUR_PART[t.waiting.kind]:"give it back"}.`,noteWhen:noteWhenWords(t,{state})}}if(state==="queued"){let m=/\((\d+) of (\d+) busy\)/.exec(t.status);return{...base2,line:t.status,sentence:`It is next in line. It starts when a slot is free${m?` (${m[1]} of ${m[2]} busy)`:""}.`,next:"Nothing is needed from you now.",noteWhen:noteWhenWords(t,{state})}}if(state==="idle")return{...base2,line:t.status,sentence:t.stage==="bugs"?"It is a reported bug, waiting for the squad.":"Nothing runs on it now.",noteWhen:noteWhenWords(t,{state})};let c=t.clock??guessClock(t),since=t.clock?.since,who4=WHO[c.who],steps=t.steps??[],part=c.who==="builder"&&c.step==="build"&&c.part&&steps.length?{n:Math.min(c.part,steps.length),of:steps.length}:void 0,usual=usualOf(book,c.who,c.step,t.track),seconds2=since?stepSeconds({...c,since},at3):void 0,fromStatus=t.status.startsWith(`${who4} is `)?t.status.slice(who4.length+4):/^Step \d+ of \d+/.test(t.status)||!t.status?"":t.status,said=t.clock?.doing??(part?steps[part.n-1]?.live:void 0)??fromStatus,raw=said.startsWith(`${who4} is `)?said.slice(who4.length+4):said,resumed=t.clock?.resumed&&!(t.clock.doingAt&&Date.parse(t.clock.doingAt)>Date.parse(t.clock.resumed)),doing=resumed?RESUMED:raw?shortDoing(raw):void 0,lastAt=t.clock?.doingAt??since;if(state==="stopping")return{...base2,who:who4,role:c.who,step:c.step,line:"Stopping\u2026",sentence:`The ${who4} is stopping. The work so far is kept.`,noteWhen:noteWhenWords(t,{state})};let line3=[who4,part?`step ${part.n} of ${part.of}`:"",doing].filter(Boolean).join(" \xB7 "),time2=seconds2!==void 0?`${durationWords(seconds2)} \xB7 ${usualWords(usual)}`:usualWords(usual),title2=part?steps[part.n-1]?.title:void 0,sentence=part&&title2?`The ${who4} is on step ${part.n} of ${part.of}: ${lowerFirst2(title2.replace(/\.$/,""))}.`:t.status.startsWith(`${who4} is `)&&c.who!=="squad"?`The ${t.status.replace(/\.$/,"")}.`:SENTENCE[c.step](t),detail=[part?`Step ${part.n} of ${part.of}`:"",seconds2!==void 0?`${durationWords(seconds2)} so far`:"",resumed?RESUMED:"",`this step usually takes ${rangeWords(usual)}`].filter(Boolean).join(" \xB7 "),go=ahead(t,c.step,part),left=Math.max(0,middle(usual)-(seconds2??0))+go.builds*middle(usualOf(book,"builder","build",t.track))+go.steps.reduce((n,s)=>n+middle(usualOf(book,s==="checks"?"squad":s==="review"?"reviewer":"tester",s,t.track)),0),then=nextWords(t,c.step,part),next=t.track==="auto"?`${then} It is accepted and merged by itself ${inAbout(left)}. Nothing is needed from you now.`:`${then} You will be asked to ${go.what} ${inAbout(left)}. Nothing is needed from you now.`,now2={...base2,who:who4,role:c.who,step:c.step,...part?{part}:{},...doing?{doing}:{},line:line3,time:time2,bar:barOf(t,c.step,part),sentence,detail,next,then,...since?{since}:{},...seconds2!==void 0?{seconds:seconds2}:{},usual,...lastAt?{lastAt}:{},readyIn:{seconds:left,what:go.what},noteWhen:""};now2.noteWhen=noteWhenWords(t,now2);let slow=slowCard(t,t.clock??{...c,doing:raw||void 0},part,lastAt,at3);return slow&&(now2.slow=slow),now2}function portOf3(url){if(!url)return null;try{let port=Number(new URL(url).port);return Number.isInteger(port)&&port>0?port:null}catch{return null}}function needsApp(t){return t.clock?.who==="tester"||/^Starting the app/.test(t.status)}async function lookAtPort(bp,t){if(!needsApp(t))return null;let url=repoFor(readMeta(bp),t).commands.url,port=portOf3(url);if(!url||!port||!await portTaken(port)||await isUp(url))return null;let ours=squadKitOn(bp,port),holder=ours?.holder??whoListens(port);return{port,ours:!!ours,...holder?{pid:holder.pid,...holder.program?{program:holder.program}:{}}:{},...ours?.app?{app:ours.app}:{}}}var LOOK_MS=2e4,looked2=new Map;function watchPort(bp,id2,now2=Date.now()){let key=`${bp.root}:${id2}`;if(now2-(looked2.get(key)??0)<LOOK_MS)return;looked2.set(key,now2);let t=readTicket(bp,id2);t?.quiet&&lookAtPort(bp,t).then(port=>{((a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null))(readTicket(bp,id2)?.quiet?.port,port)||updateTicket(bp,id2,x=>{x.quiet&&(port?x.quiet.port=port:delete x.quiet.port)})}).catch(()=>{})}async function freePortAndRetry(bp,id2,opts={}){let t=requireTicket(bp,id2),port=t.quiet?.port?.port;if(!port)throw new BoardActionError(`${t.id} is not held up by another program.`);let ours=squadKitOn(bp,port);if(!ours)throw new BoardActionError(whoListens(port)?"squad-kit did not start the program in the way, so it leaves it alone. Close it yourself, then press Stop and try again.":"Nothing squad-kit started is in the way now. Press Stop and try again.");await endPid(ours.group);let until=Date.now()+(opts.waitMs??3e3);for(;await portTaken(port)&&Date.now()<until;)await new Promise(r=>setTimeout(r,100));let what=ours.app?`your app ${ours.app}`:"a program squad-kit started earlier";if(await portTaken(port))throw new BoardActionError(`squad-kit stopped ${what}, but something is still in the way. Press Stop and try again in a moment.`);writeLog({level:"info",project:readMeta(bp).project,ticket:t.id,text:`Freed port ${port}: stopped ${ours.holder.program||"a program"} (process ${ours.holder.pid}), which squad-kit started`}),updateTicket(bp,t.id,x=>{addActivity(x,"you",`Stopped ${what}, which was in the way`),delete x.quiet});let now2=requireTicket(bp,t.id);return now2.live||now2.squad?restartStep(bp,t.id):now2}import fs114 from"fs";import path107 from"path";var troubles=new Map,checked2=new Map,SAVE_CHECK_MS=5e3,TEST_WRITE=Buffer.alloc(64*1024,32);function canSave(bp){if(!fs114.existsSync(bp.squadDir))return!1;for(let dir of[bp.squadDir,bp.localDir]){let file3=path107.join(dir,`.save-check-${process.pid}`);try{fs114.mkdirSync(dir,{recursive:!0}),fs114.writeFileSync(file3,TEST_WRITE),fs114.rmSync(file3,{force:!0})}catch{try{fs114.rmSync(file3,{force:!0})}catch{}return!1}}return!0}function isSaveTrouble(bp,err2){return fs114.existsSync(bp.squadDir)?isFileTrouble(err2)?!canSave(bp):!1:!0}function noteSaveTrouble(bp,err2){if(troubles.has(bp.squadDir))return!1;let e=asError(err2);return troubles.set(bp.squadDir,{root:bp.root,since:new Date().toISOString(),text:isFileTrouble(e)?fileTroubleWords(e):fileTroubleWords({code:"ENOENT"}),detail:e.message}),checked2.set(bp.squadDir,Date.now()),!0}function saveTroubles(){return[...troubles.values(),...turnTroubles.values()]}var turnTroubles=new Map,TURN_TROUBLE="squad-kit ran into a problem it did not expect, so a project's squad cannot move right now. Your work is safe. Restart squad-kit. If this stays, use Report a problem.";function noteTurnTrouble(bp,err2){turnTroubles.set(bp.squadDir,{root:bp.root,since:new Date().toISOString(),text:TURN_TROUBLE,detail:asError(err2).message})}function endTurnTrouble(bp){turnTroubles.delete(bp.squadDir)}function stillHeld(bp,at3=Date.now()){return troubles.has(bp.squadDir)?at3-(checked2.get(bp.squadDir)??0)<SAVE_CHECK_MS?{held:!0,fixed:!1}:(checked2.set(bp.squadDir,at3),canSave(bp)?(troubles.delete(bp.squadDir),checked2.delete(bp.squadDir),{held:!1,fixed:!0}):{held:!0,fixed:!1}):{held:!1,fixed:!1}}function forgetSaveTrouble(bp){troubles.delete(bp.squadDir),checked2.delete(bp.squadDir),turnTroubles.delete(bp.squadDir)}import fs117 from"fs";import path110 from"path";import fs115 from"fs";import path108 from"path";var SETUP_STEPS=["files","tool","read","commands","tests","app","overview"];function talkFile(bp){return path108.join(bp.localDir,"overview-talk.json")}function readTalk(bp){try{let t=JSON.parse(fs115.readFileSync(talkFile(bp),"utf8"));return Array.isArray(t.messages)?t:null}catch{return null}}function writeTalk(bp,t){fs115.mkdirSync(path108.dirname(talkFile(bp)),{recursive:!0});let tmp=`${talkFile(bp)}.${process.pid}.tmp`;fs115.writeFileSync(tmp,`${JSON.stringify(t,null,1)}
1550
+ `):void 0}var SAVING="Saving the change, then it merges",RESUMED="resumed after a restart",QUEUED2="Next in line";function queuedLine(busy,limit){return`${QUEUED2}: starts when a slot is free (${busy} of ${limit} busy)`}var TRACK=["tickets","specs","implement","test","review","release"],YOUR_PART={questions:"answer the questions",spec:"approve the spec",plan:"approve the design",proof:"check the proof",stuck:"pick a way out",release:"release it",ask:"answer the question",yours:"give it back",chat:"finish the talk",change:"decide on the design update",depends:"choose how to build it"};function lowerFirst2(s){return/^[A-Z][A-Z0-9]+\b/.test(s)?s:s.charAt(0).toLowerCase()+s.slice(1)}function upperFirst(s){return s.charAt(0).toUpperCase()+s.slice(1)}function shortDoing(line3){let s=line3.replace(/(?:[\w.@~-]*\/)+([\w.@-]+)/g,"$1").replace(/\s+/g," ").trim();return lowerFirst2(s.length>56?`${s.slice(0,55)}\u2026`:s)}function stateOf(t){return t.stage==="done"?"done":t.waiting||t.takenOver?"you":t.stopRequested?"stopping":t.status.startsWith(QUEUED2)?"queued":t.stage==="release"?"ready":t.squad||t.live?"working":"idle"}function guessClock(t){let s=t.status,doing=t.steps?.findIndex(x=>x.status==="doing")??-1;switch(t.stage){case"tickets":case"bugs":return{who:"analyst",step:"questions"};case"specs":return/^Analyst/.test(s)||t.track!=="express"&&!t.specApproved?{who:"analyst",step:"spec"}:{who:"architect",step:"design"};case"implement":return doing>=0?{who:"builder",step:"build",part:doing+1}:{who:"builder",step:/fix/i.test(s)?"fix":"build"};case"test":return/Tester|screen|screenshot/i.test(s)?{who:"tester",step:"proof"}:/Builder/.test(s)?{who:"builder",step:"fix"}:{who:"squad",step:"checks"};default:return/Builder/.test(s)?{who:"builder",step:"fix"}:/Tester|proof|screen/i.test(s)?{who:"tester",step:"proof"}:{who:"reviewer",step:"review"}}}var SENTENCE={questions:()=>"The Analyst is reading the ticket to see what it needs to ask you.",spec:t=>t.questions?.some(q4=>q4.answer)?"The Analyst is writing the spec from your answers.":'The Analyst is writing the spec: the story and the "done when" list.',design:t=>t.track==="express"?"The Architect is writing the plan.":t.track==="auto"?"The Architect is reading the part of the BRD and the code, and writing the design.":"The Architect is choosing how to build it and writing the design.",build:()=>"The Builder is writing the code.",fix:()=>"The Builder is fixing what the checks or the Reviewer found.",checks:()=>"squad-kit is running the project's own checks, so the squad never grades its own work.",review:()=>"The Reviewer, a fresh agent, is reading the changes.",proof:()=>'The Tester is opening the app and taking a screenshot for each "done when".'};function screens(t){return changesScreens(t)}var MERGES_BY_ITSELF="nothing: it is accepted and merged by itself";function ahead(t,step2,part){let after=["review",...screens(t)?["proof"]:[]],proof={what:t.track==="auto"?MERGES_BY_ITSELF:"check the proof"};switch(step2){case"questions":return{steps:[],builds:0,what:"answer its questions, or approve the spec"};case"spec":return{steps:[],builds:0,what:"approve the spec"};case"design":return t.track!=="express"&&t.track!=="auto"?{steps:[],builds:0,what:"approve the design"}:{steps:["checks",...after],builds:t.work?.steps??t.steps?.length??0,...proof};case"build":return{steps:["checks",...after],builds:part?part.of-part.n:0,...proof};case"fix":return{steps:["checks",...after],builds:0,...proof};case"checks":return{steps:after,builds:0,...proof};case"review":return{steps:screens(t)?["proof"]:[],builds:0,...proof};case"proof":return{steps:[],builds:0,...proof}}}function nextWords(t,step2,part){let shots=screens(t)?', then the Tester takes a screenshot for each "done when"':"",ready=t.track==="auto"?"The squad accepts it by itself and merges it.":"The proof is ready for you to check.";switch(step2){case"questions":return"The Analyst asks you what it needs to know, or writes the spec straight away.";case"spec":return"You read the spec and approve it. Then the Architect writes the design.";case"design":return t.track==="express"?"Express track: the plan is approved on its own, then the Builder starts and the checks and the Reviewer follow.":t.track==="auto"?"Builds by itself: the plan is approved on its own, then the Builder starts and the checks and the Reviewer follow.":"You read the design and approve it. Then the Builder starts.";case"build":return part&&part.n<part.of?`The Builder goes on with step ${part.n+1} of ${part.of}. Then the checks run on their own, a fresh Reviewer reads the work${shots}.`:`The checks run on their own, a fresh Reviewer reads the work${shots}.`;case"fix":return`The checks run again on their own, then the Reviewer reads the work${shots}.`;case"checks":return`The Reviewer, a fresh agent, reads the changes${shots}.`;case"review":return screens(t)?'The Tester takes a screenshot for each "done when".':ready;case"proof":return ready}}function inAbout(seconds2){let m=Math.round(seconds2/60);return m<=1?"in about a minute":`in about ${m} minutes`}function barOf(t,step2,part){if(part)return Array.from({length:part.of},(_,i)=>i+1<part.n?"done":i+1===part.n?"now":"todo");let group=["questions","spec","design"].includes(step2)?t.track==="express"||t.track==="auto"?["design"]:["questions","spec","design"]:["checks","review",...screens(t)?["proof"]:[]],at3=group.indexOf(step2==="fix"||step2==="build"?"checks":step2);return group.map((_,i)=>i<at3?"done":i===at3?"now":"todo")}var STEP_STAGE={questions:"tickets",spec:"specs",design:"specs",build:"implement",fix:"implement",checks:"test",review:"review",proof:"review"},LONG_WAIT_S=600,trackStageOf=s=>s==="bugs"?"tickets":s;function runsIn(t,stage){return(t.history??[]).filter(r=>trackStageOf(r.stage??STEP_STAGE[r.step])===stage)}function workIn(t,stage,at3){let runs2=runsIn(t,stage),done=runs2.reduce((n,r)=>{let secs=(Date.parse(r.to)-Date.parse(r.from))/1e3;return n+(Number.isFinite(secs)?Math.max(0,secs):0)},0),running3=t.clock&&trackStageOf(t.stage)===stage?runSeconds(t.clock,at3):void 0;return running3!==void 0?done+running3:runs2.length?done:t.history?.length&&t.history.length<HISTORY?0:void 0}function builderRunWords(t){let b=runsIn(t,"test").filter(r=>r.step==="checks").pop()?.builderRun;return b?.checks.length?b.all?" \xB7 passed earlier on the same code":` \xB7 ${b.checks.map(c=>c==="test"?"tests":c==="lint"?"code check":c).join(" and ")} passed earlier on the same code`:""}function waitWords3(t,stage,work){let spent=t.stageTimes?.spent[stage];if(spent===void 0)return;let wait=Math.max(0,spent-work),yours=Math.min(wait,t.stageTimes?.yours?.[stage]??0);return yours>=LONG_WAIT_S?`waited ${durationWords(yours)} for you`:wait>=LONG_WAIT_S?`waited ${durationWords(wait)}`:void 0}function stageNotes(t,state,book,at3){let stage=trackStageOf(t.stage),cur=stage==="done"?TRACK.length:TRACK.indexOf(stage),st=t.stageTimes,usual=(who4,step2)=>middle(usualOf(book,who4,step2,t.track)),about=s=>s>0?`about ${Math.max(1,Math.round(s/60))} min`:"";return TRACK.map((s,i)=>{let note2="",wait;if(i<cur){let work=workIn(t,s,at3),yours=t.track!=="express"&&t.track!=="auto"&&t.specApproved,said=secs=>`${durationWords(secs)}${s==="specs"&&yours?", you approved":""}`;if(work===void 0){let spent=st?.spent[s];note2=spent!==void 0?said(spent):""}else runsIn(t,s).length&&(note2=`${said(work)}${s==="test"?builderRunWords(t):""}`,wait=waitWords3(t,s,work))}else if(i===cur){let inStage=(t.clock||runsIn(t,s).length?workIn(t,s,at3):void 0)??(st?.stage===t.stage?(at3-Date.parse(st.since))/1e3:void 0);state==="you"?note2="your turn":state==="ready"?note2=t.release?`in ${t.release}`:"accepted":state==="queued"?note2="next in line":(state==="working"||state==="stopping")&&(note2=inStage!==void 0?`now \xB7 ${durationWords(inStage)}${s==="test"?builderRunWords(t):""}`:"now")}else s==="review"?note2=t.track==="auto"?"accepted by itself":"you check proof":s==="release"?note2=t.track==="auto"?"merges by itself":"on Looks right":s==="specs"?note2=about(t.track==="express"||t.track==="auto"?usual("architect","design"):usual("analyst","spec")+usual("architect","design")):s==="implement"?note2=about((t.work?.steps??t.steps?.length??0)*usual("builder","build")):s==="test"&&(note2=about(usual("squad","checks")));return{stage:s,note:note2,...wait?{wait}:{}}})}function noteWhenWords(t,now2){switch(now2.state){case"working":{let left=now2.usual?Math.max(60,middle(now2.usual)-(now2.seconds??0)):60;return now2.role==="squad"?`squad-kit is running the checks. The next agent reads it when its step starts, ${inAbout(left)}.`:`The ${now2.who} reads it when its next step starts, ${inAbout(left)}.`}case"you":return`No agent is running now, so nobody reads it yet. The squad reads it when it carries on, after you ${t.waiting?YOUR_PART[t.waiting.kind]:"give it back"}.`;case"queued":return"No agent is running now, so nobody reads it yet. It is next in line: the squad reads it when a slot is free and its step starts.";case"stopping":return"The squad is stopping. It reads it when the work carries on.";case"ready":case"done":return"The work is accepted, so no agent works on it any more. The note stays on the ticket for the record.";default:return"No agent is running now, so nobody reads it yet. The squad reads it when work on the ticket starts."}}function slowCard(t,c,part,lastAt,at3){let q4=t.quiet;if(!q4)return;let who4=WHO[c.who],where=`${t.id} \xB7 ${part?`step ${part.n} of ${part.of}`:who4}`,limit=c.limit?`If you do nothing, squad-kit stops this step at ${c.limit} minutes. All work is kept.`:void 0,port=portParts(q4),detail=slowDetail(q4);if(q4.kind==="stuck"){let quiet=Math.max(0,(at3-Date.parse(q4.since))/1e3);return{kind:"stuck",title:"Looks stuck",where,text:`No progress for ${minutesWords(quiet)}.${c.doing?` ${c.who==="squad"?"squad-kit":`The ${who4}`} was ${lowerFirst2(c.doing)}.`:""}`,facts:[{label:"What we checked",value:q4.detail},...port.fact?[port.fact]:[],{label:"Your work",value:"Safe. Everything done so far is kept."}],...limit?{limit}:{},canEnd:!!q4.pid,...port.freePort?{freePort:port.freePort}:{},...detail?{detail}:{}}}let facts=[];if(c.doing&&facts.push({label:"Doing now",value:upperFirst(c.doing)}),lastAt){let ago3=Math.max(0,(at3-Date.parse(lastAt))/1e3);facts.push({label:"Last progress",value:ago3<180?`${minutesWords(ago3)} ago, so it is still working`:`${minutesWords(ago3)} ago`})}return(t.tries??0)>0?facts.push({label:"Why it may be slow",value:`The checks failed ${t.tries===1?"once":`${t.tries} times`}, so the Builder is fixing them (try ${(t.tries??0)+1} of 3).`}):(t.reviewRound??0)>1&&facts.push({label:"Why it may be slow",value:`The Reviewer asked for changes, so this is round ${t.reviewRound} of 2.`}),port.fact&&facts.push(port.fact),{kind:"long",title:"Taking longer than usual",where,text:q4.detail,facts,...limit?{limit}:{},canEnd:!!q4.pid,...port.freePort?{freePort:port.freePort}:{},...detail?{detail}:{}}}function liveNow(t,book,at3=Date.now(),saving=!1){let state=stateOf(t),stages=stageNotes(t,state,book,at3),base2={state,stages};if(state==="done")return{...base2,line:t.status||"Released",sentence:`It is released${t.release?` in ${t.release}`:""}.`,noteWhen:noteWhenWords(t,{state})};if(saving||state==="ready"&&(t.squad||t.live)){let cur=trackStageOf(t.stage);return{...base2,stages:saving?stages.map(s=>s.stage===cur?{...s,note:"saving now"}:s):stages,merging:!0,line:saving?SAVING:t.status,sentence:saving?"You said Looks right. squad-kit is saving the change and merging it now. Nothing is needed from you.":"It is accepted, and squad-kit is merging it now. Nothing is needed from you.",noteWhen:noteWhenWords(t,{state:"ready"})}}if(state==="ready")return{...base2,line:t.status,sentence:`It is accepted and goes out with ${t.release??"the next release"}. Nothing is needed from you until then.`,noteWhen:noteWhenWords(t,{state})};if(state==="you"){let summary=t.waiting?.summary??"You took it over. Give it back when you are done";return{...base2,line:t.status,sentence:`It waits for you: ${lowerFirst2(summary)}.`,next:`The squad carries on after you ${t.waiting?YOUR_PART[t.waiting.kind]:"give it back"}.`,noteWhen:noteWhenWords(t,{state})}}if(state==="queued"){let m=/\((\d+) of (\d+) busy\)/.exec(t.status);return{...base2,line:t.status,sentence:`It is next in line. It starts when a slot is free${m?` (${m[1]} of ${m[2]} busy)`:""}.`,next:"Nothing is needed from you now.",noteWhen:noteWhenWords(t,{state})}}if(state==="idle")return{...base2,line:t.status,sentence:t.stage==="bugs"?"It is a reported bug, waiting for the squad.":"Nothing runs on it now.",noteWhen:noteWhenWords(t,{state})};let c=t.clock??guessClock(t),since=t.clock?.since,who4=WHO[c.who],steps=t.steps??[],part=c.who==="builder"&&c.step==="build"&&c.part&&steps.length?{n:Math.min(c.part,steps.length),of:steps.length}:void 0,usual=usualOf(book,c.who,c.step,t.track),seconds2=since?stepSeconds({...c,since},at3):void 0,fromStatus=t.status.startsWith(`${who4} is `)?t.status.slice(who4.length+4):/^Step \d+ of \d+/.test(t.status)||!t.status?"":t.status,said=t.clock?.doing??(part?steps[part.n-1]?.live:void 0)??fromStatus,raw=said.startsWith(`${who4} is `)?said.slice(who4.length+4):said,resumed=t.clock?.resumed&&!(t.clock.doingAt&&Date.parse(t.clock.doingAt)>Date.parse(t.clock.resumed)),doing=resumed?RESUMED:raw?shortDoing(raw):void 0,lastAt=t.clock?.doingAt??since;if(state==="stopping")return{...base2,who:who4,role:c.who,step:c.step,line:"Stopping\u2026",sentence:`The ${who4} is stopping. The work so far is kept.`,noteWhen:noteWhenWords(t,{state})};let line3=[who4,part?`step ${part.n} of ${part.of}`:"",doing].filter(Boolean).join(" \xB7 "),time2=seconds2!==void 0?`${durationWords(seconds2)} \xB7 ${usualWords(usual)}`:usualWords(usual),title2=part?steps[part.n-1]?.title:void 0,sentence=part&&title2?`The ${who4} is on step ${part.n} of ${part.of}: ${lowerFirst2(title2.replace(/\.$/,""))}.`:t.status.startsWith(`${who4} is `)&&c.who!=="squad"?`The ${t.status.replace(/\.$/,"")}.`:SENTENCE[c.step](t),detail=[part?`Step ${part.n} of ${part.of}`:"",seconds2!==void 0?`${durationWords(seconds2)} so far`:"",resumed?RESUMED:"",`this step usually takes ${rangeWords(usual)}`].filter(Boolean).join(" \xB7 "),go=ahead(t,c.step,part),left=Math.max(0,middle(usual)-(seconds2??0))+go.builds*middle(usualOf(book,"builder","build",t.track))+go.steps.reduce((n,s)=>n+middle(usualOf(book,s==="checks"?"squad":s==="review"?"reviewer":"tester",s,t.track)),0),then=nextWords(t,c.step,part),next=t.track==="auto"?`${then} It is accepted and merged by itself ${inAbout(left)}. Nothing is needed from you now.`:`${then} You will be asked to ${go.what} ${inAbout(left)}. Nothing is needed from you now.`,now2={...base2,who:who4,role:c.who,step:c.step,...part?{part}:{},...doing?{doing}:{},line:line3,time:time2,bar:barOf(t,c.step,part),sentence,detail,next,then,...since?{since}:{},...seconds2!==void 0?{seconds:seconds2}:{},usual,...lastAt?{lastAt}:{},readyIn:{seconds:left,what:go.what},noteWhen:""};now2.noteWhen=noteWhenWords(t,now2);let slow=slowCard(t,t.clock??{...c,doing:raw||void 0},part,lastAt,at3);return slow&&(now2.slow=slow),now2}function portOf3(url){if(!url)return null;try{let port=Number(new URL(url).port);return Number.isInteger(port)&&port>0?port:null}catch{return null}}function needsApp(t){return t.clock?.who==="tester"||/^Starting the app/.test(t.status)}async function lookAtPort(bp,t){if(!needsApp(t))return null;let url=repoFor(readMeta(bp),t).commands.url,port=portOf3(url);if(!url||!port||!await portTaken(port)||await isUp(url))return null;let ours=squadKitOn(bp,port),holder=ours?.holder??whoListens(port);return{port,ours:!!ours,...holder?{pid:holder.pid,...holder.program?{program:holder.program}:{}}:{},...ours?.app?{app:ours.app}:{}}}var LOOK_MS=2e4,looked2=new Map;function watchPort(bp,id2,now2=Date.now()){let key=`${bp.root}:${id2}`;if(now2-(looked2.get(key)??0)<LOOK_MS)return;looked2.set(key,now2);let t=readTicket(bp,id2);t?.quiet&&lookAtPort(bp,t).then(port=>{((a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null))(readTicket(bp,id2)?.quiet?.port,port)||updateTicket(bp,id2,x=>{x.quiet&&(port?x.quiet.port=port:delete x.quiet.port)})}).catch(()=>{})}async function freePortAndRetry(bp,id2,opts={}){let t=requireTicket(bp,id2),port=t.quiet?.port?.port;if(!port)throw new BoardActionError(`${t.id} is not held up by another program.`);let ours=squadKitOn(bp,port);if(!ours)throw new BoardActionError(whoListens(port)?"squad-kit did not start the program in the way, so it leaves it alone. Close it yourself, then press Stop and try again.":"Nothing squad-kit started is in the way now. Press Stop and try again.");await endPid(ours.group);let until=Date.now()+(opts.waitMs??3e3);for(;await portTaken(port)&&Date.now()<until;)await new Promise(r=>setTimeout(r,100));let what=ours.app?`your app ${ours.app}`:"a program squad-kit started earlier";if(await portTaken(port))throw new BoardActionError(`squad-kit stopped ${what}, but something is still in the way. Press Stop and try again in a moment.`);writeLog({level:"info",project:readMeta(bp).project,ticket:t.id,text:`Freed port ${port}: stopped ${ours.holder.program||"a program"} (process ${ours.holder.pid}), which squad-kit started`}),updateTicket(bp,t.id,x=>{addActivity(x,"you",`Stopped ${what}, which was in the way`),delete x.quiet});let now2=requireTicket(bp,t.id);return now2.live||now2.squad?restartStep(bp,t.id):now2}import fs114 from"fs";import path107 from"path";var troubles=new Map,checked2=new Map,SAVE_CHECK_MS=5e3,TEST_WRITE=Buffer.alloc(64*1024,32);function canSave(bp){if(!fs114.existsSync(bp.squadDir))return!1;for(let dir of[bp.squadDir,bp.localDir]){let file3=path107.join(dir,`.save-check-${process.pid}`);try{fs114.mkdirSync(dir,{recursive:!0}),fs114.writeFileSync(file3,TEST_WRITE),fs114.rmSync(file3,{force:!0})}catch{try{fs114.rmSync(file3,{force:!0})}catch{}return!1}}return!0}function isSaveTrouble(bp,err2){return fs114.existsSync(bp.squadDir)?isFileTrouble(err2)?!canSave(bp):!1:!0}function noteSaveTrouble(bp,err2){if(troubles.has(bp.squadDir))return!1;let e=asError(err2);return troubles.set(bp.squadDir,{root:bp.root,since:new Date().toISOString(),text:isFileTrouble(e)?fileTroubleWords(e):fileTroubleWords({code:"ENOENT"}),detail:e.message}),checked2.set(bp.squadDir,Date.now()),!0}function saveTroubles(){return[...troubles.values(),...turnTroubles.values()]}var turnTroubles=new Map,TURN_TROUBLE="squad-kit ran into a problem it did not expect, so a project's squad cannot move right now. Your work is safe. Restart squad-kit. If this stays, use Report a problem.";function noteTurnTrouble(bp,err2){turnTroubles.set(bp.squadDir,{root:bp.root,since:new Date().toISOString(),text:TURN_TROUBLE,detail:asError(err2).message})}function endTurnTrouble(bp){turnTroubles.delete(bp.squadDir)}function stillHeld(bp,at3=Date.now()){return troubles.has(bp.squadDir)?at3-(checked2.get(bp.squadDir)??0)<SAVE_CHECK_MS?{held:!0,fixed:!1}:(checked2.set(bp.squadDir,at3),canSave(bp)?(troubles.delete(bp.squadDir),checked2.delete(bp.squadDir),{held:!1,fixed:!0}):{held:!0,fixed:!1}):{held:!1,fixed:!1}}function forgetSaveTrouble(bp){troubles.delete(bp.squadDir),checked2.delete(bp.squadDir),turnTroubles.delete(bp.squadDir)}import fs117 from"fs";import path110 from"path";import fs115 from"fs";import path108 from"path";var SETUP_STEPS=["files","tool","read","commands","tests","app","overview"];function talkFile(bp){return path108.join(bp.localDir,"overview-talk.json")}function readTalk(bp){try{let t=JSON.parse(fs115.readFileSync(talkFile(bp),"utf8"));return Array.isArray(t.messages)?t:null}catch{return null}}function writeTalk(bp,t){fs115.mkdirSync(path108.dirname(talkFile(bp)),{recursive:!0});let tmp=`${talkFile(bp)}.${process.pid}.tmp`;fs115.writeFileSync(tmp,`${JSON.stringify(t,null,1)}
1550
1551
  `),fs115.renameSync(tmp,talkFile(bp))}var SETUP_USUAL={files:[1,3],tool:[1,5],read:[120,300],commands:[10,180],tests:[10,120],app:[20,120],overview:[1,3]};function newSetup(kind,startedMs,at3=Date.now()){let seconds2=Math.max(1,Math.round((at3-startedMs)/1e3));return{kind,startedAt:new Date(startedMs).toISOString(),steps:SETUP_STEPS.map(id2=>id2==="files"?{id:id2,state:"done",since:new Date(startedMs).toISOString(),seconds:seconds2}:{id:id2,state:"todo"})}}function stepOf(s,id2){let st=s.steps.find(x=>x.id===id2);return st||(st={id:id2,state:"todo"},s.steps.push(st),s.steps.sort((a,b)=>SETUP_STEPS.indexOf(a.id)-SETUP_STEPS.indexOf(b.id))),st}function begin(s,id2,note2,at3=Date.now()){let st=stepOf(s,id2);st.state="now",st.since=new Date(at3).toISOString(),delete st.seconds,delete st.error,delete st.output,note2!==void 0?st.note=note2:delete st.note}function finish(s,id2,state,extra={},at3=Date.now()){let st=stepOf(s,id2),since=st.since?Date.parse(st.since):at3;st.state=state,st.since||(st.since=new Date(at3).toISOString()),st.seconds=Math.max(state==="skipped"?0:1,Math.round((at3-since)/1e3)),extra.note!==void 0&&(st.note=extra.note),extra.error!==void 0?st.error=extra.error:delete st.error,extra.output!==void 0?st.output=extra.output:delete st.output}function readWords(files){return files?`Read the project: ${files} ${files===1?"file":"files"}`:"Read the project"}function resetFrom(s,id2){let from=SETUP_STEPS.indexOf(id2);for(let st of s.steps)SETUP_STEPS.indexOf(st.id)>=from&&(st.state="todo",delete st.since,delete st.seconds,delete st.error,delete st.output,delete st.note);delete s.readyAt,delete s.stopping,from<=SETUP_STEPS.indexOf("read")&&delete s.approvedAt,from<=SETUP_STEPS.indexOf("tests")&&(s.proof={...s.proof,tests:void 0}),from<=SETUP_STEPS.indexOf("app")&&(s.proof={...s.proof,app:void 0})}var PROOF_STEPS=["commands","tests","app","overview"];function nextProofStep(s){return!s||s.stopping||s.steps.find(x=>x.id==="read")?.state!=="done"||s.steps.some(x=>x.state==="now"||x.state==="stopped")?null:PROOF_STEPS.find(id2=>stepOf(s,id2).state==="todo")??null}function currentStep(s){let running3=s.steps.find(x=>x.state==="now");if(running3)return running3;let read6=s.steps.find(x=>x.id==="read");return read6?.state==="failed"?read6:s.steps.find(x=>x.state==="todo"||x.state==="stopped")}function setupOver(s){return s.steps.every(x=>x.state!=="todo"&&x.state!=="now"&&x.state!=="stopped")}var LEARN_AFTER2=3;function usualSetupKey(id2){return`setup.${id2}`}function setupUsual(bp,id2){let runs2=bp?readUsual(bp)[usualSetupKey(id2)]??[]:[];if(runs2.length<LEARN_AFTER2){let[low,high]=SETUP_USUAL[id2];return{low,high,learned:!1}}let sorted=[...runs2].sort((a,b)=>a-b),q4=p=>{let at3=(sorted.length-1)*p,lo=Math.floor(at3),hi=Math.ceil(at3);return sorted[lo]+(sorted[hi]-sorted[lo])*(at3-lo)};return{low:q4(.25),high:q4(.75),learned:!0}}function learnSetupTime(bp,st){st.state==="done"&&st.seconds&&st.seconds>1&&recordUsual(bp,usualSetupKey(st.id),st.seconds)}function usualText(u){return`usually ${rangeWords(u)}`}import fs116 from"fs";import path109 from"path";var STOPPED8="You stopped the squad.";function withSetup(bp,change){let meta=readMeta(bp);meta.setup&&(change(meta.setup,meta),writeMeta(bp,meta))}function clean2(c){return Object.fromEntries(Object.entries(c??{}).filter(([,v])=>typeof v=="string"&&v.trim()))}var TECHNICAL_IDEA=/\b(tech(nology)?\s*stack|the\s+stack|technolog(y|ies)|framework|programming\s+language|database|repo(sitory)?|boilerplate|scaffold|set\s*up|install|tooling|hosting|deploy)\b/i;function noCodeAnswer(out18,idea){let ideas=idea?(out18.ideas??[]).filter(i=>i?.title&&!TECHNICAL_IDEA.test(`${i.title} ${i.why??""}`)):[];return{empty:!0,summary:out18.summary,map:[],decisions:[],commands:{},users:"",can:[],worth:[],ideas,parts:[],links:[]}}function withAllTests(dir,c){let test=withBrowserTests(dir,c.test);return test?{...c,test}:c}var FIRST_SAVE_WORDS="Saving your files for the first time. On a big project this takes a while.";function firstSaveHolds(meta){return!!meta.setup?.steps.some(s=>s.id==="files"&&s.state!=="done")}function firstSaveDue(meta){return!!meta.setup?.steps.some(s=>s.id==="files"&&(s.state==="todo"||s.state==="now"))}async function runFirstSave(bp,signal2){withSetup(bp,s=>begin(s,"files",FIRST_SAVE_WORDS));try{await firstSaveLater(bp.root,signal2),withSetup(bp,s=>{finish(s,"files","done"),delete stepOf(s,"files").note})}catch(err2){if(signal2.aborted)return;withSetup(bp,s=>finish(s,"files","failed",{error:plainWords(err2,"Saving the files for the first time","something technical stopped it. Press Try again.")}))}}async function readStarted(bp,tool5){let[files,here]=await Promise.all([countFiles(bp.root),gitState(bp.root)]);withSetup(bp,s=>{let t=stepOf(s,"tool");t.state!=="done"&&(t.state!=="now"&&begin(s,"tool"),tool5.ready?finish(s,"tool","done",{note:"The squad works with it"}):finish(s,"tool","failed",{error:`${tool5.label} is not installed on this computer. Install it and sign in once, then press Try again.`})),begin(s,"read","Starting\u2026"),s.reads=[],s.proof={...s.proof,files,read:0,...here}})}function readingNow(bp,line3,opened,read6=line3){withSetup(bp,s=>{let st=stepOf(s,"read");st.state==="now"&&(st.note=`${line3}${opened?` \xB7 ${opened} ${opened===1?"file":"files"}`:""}`,s.reads?.at(-1)!==read6&&(s.reads=[...s.reads??[],read6].slice(-8)),s.proof={...s.proof,read:opened})})}function settleSetupStop(bp){withSetup(bp,s=>{if(!s.stopping)return;let step2=s.steps.find(x=>x.state==="now")??currentStep(s);step2&&(step2.state="stopped",step2.note="You stopped it."),delete s.stopping});let meta=readMeta(bp);(meta.scan?.status==="pending"||meta.scan?.status==="running")&&writeMeta(bp,{...meta,scan:{status:"failed",at:now(),error:"You stopped it. Press Start again when you are ready."}})}function readFailed(bp,error2){withSetup(bp,s=>{let stopped=!!s.stopping||error2===STOPPED8;finish(s,"read",stopped?"stopped":"failed",stopped?{note:"You stopped it."}:{error:error2}),delete s.stopping})}function saveScan(bp,answer2,nested,at3=now(),idea=""){let fresh=readMeta(bp),out18=answer2.empty||fresh.setup?.kind==="empty"&&nextToNothing(bp.root)?noCodeAnswer(answer2,idea):answer2;fresh.map=out18.map??[],fresh.commands={...withAllTests(bp.root,clean2(out18.commands)),...clean2(fresh.commands??{})},nested.length&&(fresh.repos=nested.map(r=>{let found3=out18.repos?.find(x=>x.path.replace(/\/$/,"")===r.path),kept=fresh.repos?.find(x=>x.path===r.path);return{...r,summary:found3?.summary??kept?.summary,commands:{...withAllTests(path109.join(bp.root,r.path),clean2(found3?.commands??{})),...clean2(kept?.commands??{})}}}));let parts2=partsAfterRead(cleanParts(out18.runs,fresh,bp.root),fresh.parts);if(parts2.length?fresh.parts=parts2:delete fresh.parts,fresh.scan={status:"done",at:at3,summary:out18.summary,empty:out18.empty},fresh.setup){let s=fresh.setup;if(finish(s,"read","done",{note:readWords(s.proof?.files)}),learnSetupTime(bp,stepOf(s,"read")),out18.empty)for(let id2 of["commands","tests","app"])finish(s,id2,"skipped",{note:"Nothing to run yet: the project has no code"},Date.parse(at3))}writeMeta(bp,fresh),out18.empty||learnAfterRead(bp);let before=readOverview(bp);writeOverview(bp,{...cleanOverview(out18),readAt:at3,...before.approved&&!fresh.setup?{approved:before.approved}:{}});let found2=(out18.decisions??[]).filter(d=>d?.title?.trim()),titles=new Set(found2.map(d=>d.title.trim()));for(let d of listDecisions(bp))d.proposed&&!titles.has(d.title)&&fs116.rmSync(path109.join(bp.decisionsDir,`${d.id}.md`),{force:!0});let existing=listDecisions(bp);for(let d of found2)existing.some(x=>x.title===d.title.trim())||writeDecision(bp,{id:nextDecisionId(bp),title:d.title.trim(),why:d.why??"",options:[d.title.trim()],chosen:d.title.trim(),from:"Project scan",approvedBy:"",proposed:!0,date:at3,checkedBy:null,tickets:[]})}function setupCopyDir(bp,repo){return path109.join(worktreeRoot(path109.join(bp.root,repo.path)),"setup-copy")}async function ensureCopy(bp,repo,signal2){await droppingSetup.get(bp.squadDir);let dir=setupCopyDir(bp,repo);if(copyIsWhole(dir))return placeStoryFiles(bp,repo.path,dir),{dir,fresh:!1};let src=path109.join(bp.root,repo.path),commit=git(src,["rev-parse","--verify",`${repo.base}^{commit}`],{allowFail:!0})||git(src,["rev-parse","HEAD"]);return await addSnapshot(src,path109.basename(dir),commit,{signal:signal2}),await fillOwnCopy(bp,repo.path,dir),{dir,fresh:!0}}function guardFor(bp,cwd){if(!guardSupport().on)return;let allow=readMeta(bp).guard;return projectGuardFor(bp,{cwd,...allow?{allow}:{}})}function dropSetupCopies(bp){let run6=(droppingSetup.get(bp.squadDir)??Promise.resolve()).then(async()=>{let meta;try{meta=readMeta(bp)}catch{return}for(let repo of reposOf(meta)){let dir=setupCopyDir(bp,repo);fs116.existsSync(dir)&&await dropSnapshot(path109.join(bp.root,repo.path),dir)}});return droppingSetup.set(bp.squadDir,run6.catch(()=>{})),run6}var droppingSetup=new Map;function commandsLine(meta){let repos=reposOf(meta),tests=repos.some(r=>r.commands.test?.trim()),start=repos.some(r=>r.commands.start?.trim());return tests&&start?"Found how to run the tests and start the app":tests?"Found how to run the tests":start?"Found how to start the app":"Found no tests and no app to start yet."}var commandsStep=async(bp,deps,copies)=>{withSetup(bp,s=>begin(s,"commands","Making squad-kit's own copy of the code. On a big project this takes a while."));let meta=readMeta(bp),failures=[];for(let repo of reposOf(meta).filter(r=>repoInUse(meta,r.path))){let copy2;try{copy2=await ensureCopy(bp,repo,deps.signal)}catch(err2){if(err2 instanceof GitStopped)throw err2;failures.push({error:err2 instanceof NoRoomForCopy?err2.message:`squad-kit could not make its copy of the code: ${err2.message}`});continue}copies.set(repo.path,copy2.dir);let setup=repo.commands.setup?.trim();if(!setup)continue;withSetup(bp,s=>stepOf(s,"commands").note="Installing what it needs");let r=await runCommand(copy2.dir,setup,SETUP_MS,{silentMs:CHECK_SILENT_MS,signal:deps.signal,...guardFor(bp,copy2.dir)?{guard:guardFor(bp,copy2.dir)}:{}});if(deps.signal.aborted)throw new Error(STOPPED8);r.ok||failures.push({error:dockerBlockedWords(r.output,guardFor(bp,copy2.dir))??"Installing what it needs did not work.",output:`${setup}:
1551
1552
  ${tail(r.output,30)}`})}let fresh=readMeta(bp);withSetup(bp,s=>{let f=failures[0];f?finish(s,"commands","failed",{note:commandsLine(fresh),error:f.error,...f.output?{output:f.output}:{}}):finish(s,"commands","done",{note:commandsLine(fresh)}),learnSetupTime(bp,stepOf(s,"commands"))})};async function copyFor(bp,repo,copies,signal2){let known=copies.get(repo.path);if(known)return known;let copy2=await ensureCopy(bp,repo,signal2);copies.set(repo.path,copy2.dir);let setup=repo.commands.setup?.trim();return copy2.fresh&&setup&&await runCommand(copy2.dir,setup,SETUP_MS,{silentMs:CHECK_SILENT_MS,signal:signal2,...guardFor(bp,copy2.dir)?{guard:guardFor(bp,copy2.dir)}:{}}),copy2.dir}var testsStep=async(bp,deps,copies)=>{let meta=readMeta(bp),repos=reposOf(meta).filter(r=>r.commands.test?.trim()&&repoInUse(meta,r.path));if(!repos.length){withSetup(bp,s=>{begin(s,"tests"),finish(s,"tests","skipped",{note:"No tests found. Your first ticket can add some."})});return}let runs2=[];for(let repo of repos){let command=repo.commands.test.trim();withSetup(bp,s=>begin(s,"tests",DOING.tests));let dir;try{dir=await copyFor(bp,repo,copies,deps.signal)}catch(err2){runs2.push({ok:!1,command,repo:repo.path,output:err2.message});continue}let r=await runCommand(dir,command,CHECK_MS,{silentMs:CHECK_SILENT_MS,signal:deps.signal,...guardFor(bp,dir)?{guard:guardFor(bp,dir)}:{}});if(deps.signal.aborted)throw new Error(STOPPED8);let passed2=testCount(r.output);runs2.push({ok:r.ok,command,repo:repo.path,seconds:Math.round(r.seconds),...passed2!==null?{passed:passed2}:{},...r.ok?{}:{output:tail(r.output,30)}})}let failed2=runs2.filter(x=>!x.ok),passed=runs2.reduce((n,x)=>n+(x.passed??0),0);withSetup(bp,s=>{if(s.proof={...s.proof,tests:runs2},failed2.length){let f=failed2[0];finish(s,"tests","failed",{note:runs2.length>1?`${failed2.length} of ${runs2.length} test runs did not pass`:"Your tests did not pass here",error:"The tests did not pass in squad-kit's copy. The setup goes on: this does not stop you.",output:`${f.command}:
1552
1553
  ${f.output??""}`})}else finish(s,"tests","done",{note:passed?`Ran your tests: ${passed} pass`:"Ran your tests: they pass"});learnSetupTime(bp,stepOf(s,"tests"))})},appStep=async(bp,deps,copies)=>{let meta=readMeta(bp),repo=reposOf(meta).find(r=>r.commands.start?.trim()&&r.commands.url?.trim()&&repoInUse(meta,r.path));if(!repo){withSetup(bp,s=>{begin(s,"app"),finish(s,"app","skipped",{note:"No app to open yet."})});return}let start=repo.commands.start.trim(),url=repo.commands.url.trim();withSetup(bp,s=>begin(s,"app",DOING.start));let app=null,dir="";try{dir=await copyFor(bp,repo,copies,deps.signal),app=await startApp(dir,start,url,APP_WAIT_MS,deps.signal,guardFor(bp,dir));let at3=app.url;withSetup(bp,s=>stepOf(s,"app").note="Taking a screenshot"),fs116.mkdirSync(bp.proofDir,{recursive:!0});let file3=path109.join(bp.proofDir,"setup-app.png");fs116.rmSync(file3,{force:!0});let shot;try{shot=await deps.shoot({cwd:dir,url:at3,file:file3,onLine:t=>withSetup(bp,s=>stepOf(s,"app").note=t)})}catch(err2){if(deps.signal.aborted)throw err2;shot={why:err2.message}}let saved=fs116.existsSync(file3)?path109.basename(file3):void 0;withSetup(bp,s=>{s.proof={...s.proof,app:{ok:!0,url:at3,command:start,repo:repo.path,...saved?{shot:saved}:{}}},finish(s,"app","done",{note:saved?"Opened the app":"Opened the app. No screenshot this time.",...saved?{}:{output:`${at3}: ${(shot.why??"squad-kit saved none").slice(0,400)}`}}),learnSetupTime(bp,stepOf(s,"app"))})}catch(err2){if(deps.signal.aborted)throw err2;let error2=err2.message;withSetup(bp,s=>{s.proof={...s.proof,app:{ok:!1,url,command:start,repo:repo.path,error:error2}},finish(s,"app","failed",{note:"The app did not open here",error:(dir?dockerBlockedWords(error2,guardFor(bp,dir)):null)??"squad-kit could not start the app in its own copy. The setup goes on: this does not stop you.",output:error2.slice(-4e3)})})}finally{app?.stop()}},overviewStep=async bp=>{withSetup(bp,(s,meta)=>{begin(s,"overview");let empty=readTalk(bp)?.idea?"Your first stories, for you to check":"One question for you: what do you want to build?";finish(s,"overview","done",{note:meta.scan?.empty?empty:"What squad-kit understood, for you to check"}),s.readyAt=now()})},STEPS3={commands:commandsStep,tests:testsStep,app:appStep,overview:overviewStep};function resumeSetup(bp){withSetup(bp,s=>{for(let st of s.steps)st.state==="now"&&st.id!=="read"&&(st.state="todo",delete st.since);delete s.stopping})}async function runSetupProof(bp,deps){let copies=new Map;for(;;){let id2=nextProofStep(readMeta(bp).setup);if(!id2)return;try{if(deps.signal.aborted)throw new Error(STOPPED8);await STEPS3[id2](bp,deps,copies)}catch(err2){let stopped=deps.signal.aborted||err2.message===STOPPED8;if(withSetup(bp,s=>{stopped?finish(s,id2,"stopped",{note:"You stopped it."}):finish(s,id2,"failed",{error:err2.message}),delete s.stopping}),stopped)return}}}var LABEL={files:(_t,s)=>s==="done"?"Project memory created":"Saving your files",tool:(t,s)=>s==="done"?`${t} is ready`:s==="failed"?`${t} is not ready`:`Checking ${t}`,read:(_t,s)=>s==="done"?"The Architect read your code":s==="now"?"The Architect is reading your code":"The Architect reads your code",commands:()=>"Finding how to test and run it",tests:()=>"Trying the tests",app:()=>"Opening the app once",overview:()=>"Your project overview"},IDEA_READ={todo:"The Architect reads what you want to build",now:"The Architect is reading what you want to build",done:"The Architect read what you want to build"},HINT={files:"A .squad folder in your project. Your code is not changed.",tool:"The AI tool the squad works with",read:"What the project is, its parts and the rules it follows",commands:"In squad-kit's own copy. Your folder is not touched.",tests:"squad-kit runs your tests once",app:"squad-kit opens your app once, with a screenshot",overview:"What squad-kit understood, for you to check"},again=(step2,label3="Try again")=>({do:"again",step:step2,label:label3,command:`squad setup --again ${step2}`});function waysOf(id2,state,output){if(state==="stopped")return[again(id2==="tool"?"read":id2,"Start again")];let seeOutput=output?[{do:"output",label:"See what failed"}]:[];return state==="failed"?id2==="tool"||id2==="read"?[again("read")]:id2==="app"?[again("app"),...seeOutput,{do:"settings",label:"Set how it starts"}]:[again(id2),...seeOutput]:state==="skipped"&&id2==="app"?[{do:"settings",label:"Set how it starts"}]:[]}function toolLabel2(bp){try{return aiTool(readAgent(bp,"architect").tool)?.label??"The AI tool"}catch{return"The AI tool"}}function setupView(bp){let meta=readMeta(bp),tool5=toolLabel2(bp),s=meta.setup;if(!s){if(!meta.scan||meta.scan.status==="done")return null;let read6=meta.scan.status==="running"?"now":meta.scan.status==="failed"?"failed":"todo";s={kind:"repo",startedAt:meta.scan.at??now(),steps:[{id:"files",state:"done"},{id:"read",state:read6,...meta.scan.now?{note:meta.scan.now}:{},...meta.scan.error?{error:meta.scan.error}:{},...meta.scan.at?{since:meta.scan.at}:{}}]}}let idea=!!readTalk(bp)?.idea,steps=s.steps.filter(st=>SETUP_STEPS.includes(st.id)).map(st=>({id:st.id,label:st.id==="read"&&idea?IDEA_READ[st.state==="done"||st.state==="now"?st.state:"todo"]:LABEL[st.id](tool5,st.state),state:st.state,note:st.note??(st.state==="todo"||st.id==="files"?HINT[st.id]:void 0),...st.error?{error:st.error}:{},...st.output?{output:st.output}:{},...st.since?{since:st.since}:{},...st.seconds!==void 0?{seconds:st.seconds}:{},usual:usualText(setupUsual(bp,st.id)),ways:waysOf(st.id,st.state,st.output)})),over=setupOver(s),brd=s.fromBrd&&meta.scan?.empty?listBreakdowns(bp)[0]:void 0;return{project:meta.project,engine:meta.engine,kind:s.kind,startedAt:s.startedAt,steps,done:s.steps.filter(x=>x.state!=="todo"&&x.state!=="now"&&x.state!=="stopped").length,total:steps.length,...currentStep(s)?{current:currentStep(s).id}:{},ready:over&&!!meta.setup,approved:!!s.approvedAt,stopping:!!s.stopping,reads:s.reads??[],quality:meta.quality??DEFAULT_QUALITY,qualityAsked:!!s.qualityAt,tool:tool5,empty:!!meta.scan?.empty,idea,...brd?{brd:{id:brd.id,title:brd.title}}:{}}}function setupLine(v){if(v.approved)return"Set up";if(v.ready)return v.empty?v.idea?"Your first stories are ready to check":"One question for you: what do you want to build?":"Your project overview is ready to check";let cur=v.steps.find(x=>x.id===v.current);return cur?.state==="stopped"?`Stopped at: ${cur.label.toLowerCase()}`:cur?.state==="failed"?`${cur.label}: ${cur.error??"it did not work"}`:`Getting to know the project: step ${v.steps.findIndex(x=>x.id===v.current)+1} of ${v.total}${cur?` \xB7 ${cur.label}`:""}`}function requireSetup(bp){let meta=readMeta(bp);if(!meta.setup)throw new BoardActionError("This project has no setup running. To read it again, press Read it again on the Overview.");return meta}function stopSetup(bp){let meta=requireSetup(bp);if(setupOver(meta.setup))throw new BoardActionError("The setup is over: there is nothing to stop.");if(meta.setup.stopping=!0,!meta.setup.steps.some(x=>x.state==="now")){let next=currentStep(meta.setup);next&&(next.state="stopped",next.note="You stopped it."),next?.id==="read"&&meta.scan?.status==="pending"&&(meta.scan={status:"failed",at:now(),error:"You stopped it. Press Start again when you are ready."}),delete meta.setup.stopping}return writeMeta(bp,meta),setupView(bp)}function setupAgain(bp,step2="read"){let saveAgain=step2==="files"&&readMeta(bp).setup?.steps.some(x=>x.id==="files"&&x.state!=="done");if(!SETUP_STEPS.includes(step2)||step2==="files"&&!saveAgain)throw new BoardActionError(`"${step2}" is not a step to run again. Use tool, read, commands, tests or app.`);let meta=readMeta(bp),from=step2==="tool"?"read":step2;if(!meta.setup){if(from!=="read")throw new BoardActionError("This project has no setup yet. Read it again first.");meta.setup={kind:"project",startedAt:now(),steps:SETUP_STEPS.map(id2=>({id:id2,state:id2==="files"?"done":"todo"}))}}if(meta.setup.steps.some(x=>x.state==="now"))throw new BoardActionError("A step is still running. Wait for it, or press Stop first.");return resetFrom(meta.setup,from),(from==="read"||from==="files")&&(stepOf(meta.setup,"tool").state="todo",meta.scan={status:"pending"},meta.engine==="live"&&trackTask(dropSetupCopies(bp))),writeMeta(bp,meta),setupView(bp)}function setupQuality(bp,value){let q4=parseQuality(value);if(!q4)throw new BoardActionError(`"${value}" is not a Quality setting. Use best, balanced or thrifty.`);let meta=readMeta(bp);return meta.quality=q4,meta.setup&&(meta.setup.qualityAt=now()),writeMeta(bp,meta),setupView(bp)}function mergedAt(t){return t.mergedAt?t.mergedAt:t.stage==="release"||t.stage==="done"?t.activity[0]?.at??t.createdAt:void 0}function overviewView(bp){let meta=readMeta(bp),o=readOverview(bp),num4=id2=>Number(/(\d+)$/.exec(id2)?.[1]??0),decisions=listDecisions(bp).filter(d=>!d.replacedBy).sort((a,b)=>String(a.date).localeCompare(String(b.date))||num4(a.id)-num4(b.id)),rules=[...decisions.filter(d=>d.from==="Project scan"),...decisions.filter(d=>d.from!=="Project scan")].map(d=>({id:d.id,title:d.title,chosen:d.chosen,why:d.why,proposed:!!d.proposed,fromScan:d.from==="Project scan"})),proposed=rules.filter(r=>r.proposed).length,sv=setupView(bp),s=meta.setup,handover=s&&!s.approvedAt?setupOver(s)?"ready":"reading":proposed?"ready":o.approved||s?.approvedAt?"approved":sv?"reading":"none",readAt=o.readAt??meta.scan?.at,since=listTickets(bp).map(t=>({id:t.id,title:t.title,at:mergedAt(t)})).filter(x=>!!x.at&&(!readAt||x.at>readAt)).sort((a,b)=>b.at.localeCompare(a.at)).slice(0,8),talk2=readTalk(bp);return{project:meta.project,engine:meta.engine,user:meta.user.name,...meta.scan?.summary?{summary:meta.scan.summary}:{},...readAt?{readAt}:{},...o.refreshedAt?{refreshedAt:o.refreshedAt}:{},...o.users?{users:o.users}:{},can:o.can,worth:o.worth,ideas:o.ideas,...pictureInUse(meta,o),...productLine(meta)?{product:productLine(meta)}:{},map:meta.map,rules,proposed,...s?.proof?{proof:s.proof}:{},handover,...o.approved?{approved:o.approved}:s?.approvedAt&&!s.fromBrd?{approved:{by:meta.user.name,at:s.approvedAt}}:{},since,...sv&&!sv.ready&&!sv.approved?{setup:{done:sv.done,total:sv.total,line:setupLine(sv)}}:{},...sv?.brd?{brd:sv.brd}:{},...talk2?{talk:talk2}:{},empty:!!meta.scan?.empty||meta.scan?.status!=="done"&&meta.setup?.kind==="empty"&&!meta.setup.approvedAt,...talk2?.idea?{idea:talk2.idea}:{},old:!fs117.existsSync(path110.join(bp.knowledgeDir,"overview.md"))&&meta.scan?.status==="done",command:"squad overview"}}function approveOverview(bp){let meta=readMeta(bp);if(meta.setup&&!meta.setup.approvedAt&&!setupOver(meta.setup))throw new BoardActionError("squad-kit is still getting to know the project. Approve it once the overview is ready.");let by2=meta.user.name,at3=now();for(let d of listDecisions(bp).filter(x=>x.proposed)){let{proposed:_p,...rule}=d;writeDecision(bp,{...rule,approvedBy:by2,date:at3})}writeOverview(bp,{...readOverview(bp),approved:{by:by2,at:at3}}),meta.setup&&(meta.setup.approvedAt=at3,writeMeta(bp,meta));let talk2=readTalk(bp);return talk2?.open&&writeTalk(bp,{...talk2,open:!1,thinking:!1}),meta.engine==="live"&&trackTask(dropSetupCopies(bp)),overviewView(bp)}function ticketFromIdea(bp,n){let o=readOverview(bp),idea=o.ideas[n-1];if(!idea)throw new BoardActionError(`There is no idea ${n}. The overview has ${o.ideas.length}.`);let made=idea.ticket?readTicket(bp,idea.ticket):null;if(made)return made;let t=createTicket(bp,{title:idea.title,ask:idea.why?`${idea.title}.
@@ -1702,8 +1703,9 @@ decided: small things you decided while redoing it, one sentence each, with item
1702
1703
  ${PLAIN2}`}function allStoryLines(b){return b.epics.flatMap(e=>[...e.features.flatMap(f=>f.stories),...e.stories]).map(storyLine)}var MINUTE=6e4,BRD_READ_MS=30*MINUTE,BRD_WRITE_MS=15*MINUTE,BRD_ANSWER_MS=5*MINUTE,LONGEST_READ_MS=240*MINUTE;function readLimitMs(source){let usual=usualReadSeconds(source.kind==="pdf"?{pages:source.pages??0,parts:0}:{parts:source.pages??1});return Math.min(LONGEST_READ_MS,Math.max(BRD_READ_MS,usual*2e3))}var READ_AGAIN=" Try again reads the document from the first page, and uses your AI again.",NO_ANSWER="Could not answer this time. Ask again in a moment.",Stopped=class extends Error{},HeldAtBudget=class extends Error{};function usageKey(bp,step2){try{let agent=readAgent(bp,"analyst"),lv=resolveLevel({role:"analyst",step:step2,quality:readMeta(bp).quality},agent.levels,personLevels());return{role:"analyst",step:step2,level:lv.level,model:lv.model||aiTool(agent.tool)?.modelFor(lv.level)||""}}catch{return{role:"analyst",step:step2,level:"strongest",model:""}}}function cleanRef(r){if(!r)return;let page=Number(r.page),out18={...r.section?.toString().trim()?{section:r.section.toString().trim()}:{},...r.title?.toString().trim()?{title:r.title.toString().trim()}:{},...Number.isFinite(page)&&page>0?{page:Math.round(page)}:{}};return Object.keys(out18).length?out18:void 0}function cleanSize2(s){let v=String(s??"").trim().toUpperCase();return v==="S"||v==="L"?v:"M"}function story(o,key){return{key,title:String(o.title??"").trim()||"Untitled story",story:String(o.story??"").trim(),criteria:(o.criteria??[]).map(c=>({given:String(c.given??"").trim(),when:String(c.when??"").trim(),then:String(c.then??"").trim()})).filter(c=>c.given&&c.when&&c.then).slice(0,6),size:cleanSize2(o.size),dependsOn:[],...cleanRef(o.ref)?{ref:cleanRef(o.ref)}:{}}}function resolveNeeds(b,made,map){let known=new Set(allStories(b).map(x=>x.story.key));for(let{s,needs}of made){let out18=(needs??[]).map(n=>String(n).trim().toUpperCase()).map(n=>map.get(n)??(known.has(n)?n:"")).filter(n=>n&&n!==s.key);s.dependsOn=[...new Set(out18)]}}function cutLoops(b){let cut=0;for(let loop=findLoop(b);loop&&cut<50;loop=findLoop(b)){let from=findItem(b,loop[loop.length-2])?.story;if(!from)break;from.dependsOn=from.dependsOn.filter(n=>n!==loop[loop.length-1]),cut+=1}return cut}function addDecided2(b,list2,map,byOf){let n=0;for(let d of list2??[]){let text3=String(d.text??"").trim();if(!text3||b.decided.some(x=>x.text===text3))continue;let raw=d.item?String(d.item).trim().toUpperCase():"",item=map.get(raw)??(raw&&findItem(b,raw)?raw:void 0);b.decided.push({id:nextDecidedId(b.decided),text:text3,by:byOf(d),at:now(),...item?{item}:{}}),n+=1}return n}function byFor(b){return d=>{let q4=d.question?b.questions.find(x=>x.id===String(d.question).trim().toLowerCase()):void 0;return q4?.answer&&q4.answer.choice!==SKIP_CHOICE?"answer":"analyst"}}function documentFiles(bp,b){let dir=breakdownDir(bp,b.id),media=breakdownMediaDir(bp,b.id);if(b.source.kind==="pdf"?!fs123.existsSync(path115.join(dir,b.source.file)):!textParts(dir).length)throw new DocumentError(`The document of ${b.id} is no longer in its folder. Add the BRD again.`);return{kind:b.source.kind,...b.source.kind==="pdf"?{pdf:path115.join(dir,b.source.file),...b.source.pages?{pages:b.source.pages}:{}}:{},parts:textParts(dir).map(p=>path115.join(dir,p)),...fs123.existsSync(media)&&fs123.readdirSync(media).length?{media}:{},extra:(b.source.extra??[]).map(f=>path115.join(dir,f))}}function readUpTo(e,kind){let file3=[e.input.file_path,e.input.path].find(p=>typeof p=="string")??"";if(kind!=="pdf")return Number(/part-(\d+)\.md$/.exec(file3)?.[1]??0);if(!/\.pdf$/i.test(file3))return 0;let nums=String(e.input.pages??"").match(/\d+/g)?.map(Number)??[];return nums.length?Math.max(...nums):0}function foundSoFar(text3){let m=[...text3.matchAll(/found so far:\s*(\d+)\s*epics?,?\s*(?:and\s*)?(\d+)\s*features?/gi)].at(-1);return m?{epics:Number(m[1]),features:Number(m[2])}:null}function goOn2(ctx){if(ctx.deps.signal?.aborted||readBreakdownState(ctx.bp,ctx.id).job?.status==="stopping")throw new Stopped}function setNow(ctx,text3,more3={}){updateJob(ctx.bp,ctx.id,j=>{j.status!=="stopping"&&Object.assign(j,{now:text3,...more3})})}async function ask(ctx,a){goOn2(ctx);let held4=await ctx.deps.budgetHold?.();if(held4)throw new HeldAtBudget(held4);let more3=readBreakdownState(ctx.bp,ctx.id).capRaise??0,key=usageKey(ctx.bp,a.step),kept=run6=>updateBreakdownState(ctx.bp,ctx.id,s=>{s.usage=addRun(s.usage,key,run6.usage);let session=run6.sessionId??a.sessionId;session&&(a.sessionId||a.resume||!s.sessions?.read)&&(s.sessions={...s.sessions,read:session})}),r;try{r=await ctx.deps.run({...a,...more3?{more:more3}:{}})}catch(err2){throw err2 instanceof CapReached&&err2.run&&kept(err2.run),err2}return kept(r),more3&&updateBreakdownState(ctx.bp,ctx.id,s=>delete s.capRaise),goOn2(ctx),r}function counts(b){return{epics:b.epics.length,features:b.epics.reduce((n,e)=>n+e.features.length,0),stories:allStories(b).length}}async function textStep(ctx,b){setNow(ctx,"Turning the document into text");let p=prepareText(breakdownDir(ctx.bp,ctx.id),b.source);return updateBreakdown(ctx.bp,ctx.id,x=>{x.source.pages=p.pages??(p.parts||void 0),p.pictures!==void 0&&(x.source.pictures=p.pictures)}),updateBreakdownState(ctx.bp,ctx.id,s=>{s.job={step:"read",status:"running",startedAt:now(),now:"Reading the document",at:{done:0,of:p.pages??p.parts,unit:b.source.kind==="pdf"?"page":"part"},usualSeconds:usualReadSeconds(p)},addBreakdownActivity(s,"squad","Turned the document into text")}),!0}async function readStep(ctx,b,job){let docs=documentFiles(ctx.bp,b),unit=b.source.kind==="pdf"?"page":"part",of=b.source.kind==="pdf"?b.source.pages??0:docs.parts.length,done=0,said="",found2={epics:0,features:0};setNow(ctx,"Reading the document",{at:{done:0,of,unit}});let session=job.more?readBreakdownState(ctx.bp,ctx.id).sessions?.read:void 0,out18=(await ask(ctx,{step:"questions",prompt:session?carryOnPrompt2():readPrompt(ctx.project,b,docs),schema:READ_SCHEMA,...session?{resume:session}:{sessionId:randomUUID6()},timeoutMs:readLimitMs(b.source),onTool:e=>{let n=Math.min(of||1/0,readUpTo(e,b.source.kind));n>done&&(done=n,setNow(ctx,"Reading the document",{at:{done,of,unit}}))},onText:t=>{said=(said+t).slice(-4e3);let f=foundSoFar(said);f&&(f.epics!==found2.epics||f.features!==found2.features)&&(found2=f,setNow(ctx,"Reading the document",{found:{...f,stories:0}}))}})).structured,questions=(out18.questions??[]).slice(0,MAX_QUESTIONS).map((q4,i)=>{let options=(q4.options??[]).map(o=>String(o).trim()).filter(Boolean).slice(0,4),typed=q4.typedOption?.trim(),refs=(q4.refs??[]).map(cleanRef).filter(x=>!!x);return{id:`q${i+1}`,from:"analyst",kind:q4.kind==="clash"?"clash":"gap",text:String(q4.text??"").trim(),options,...typed&&options.includes(typed)?{inputOption:typed}:{},...refs.length?{refs}:{},...q4.impact?.trim()?{impact:q4.impact.trim()}:{}}}),epics=(out18.epics??[]).length,features=(out18.epics??[]).reduce((n,e)=>n+(e.features?.length??0),0),decided=0,after=updateBreakdown(ctx.bp,ctx.id,x=>{x.summary=String(out18.summary??"").trim()||x.summary,x.questions=questions,decided=addDecided2(x,out18.decided,new Map,()=>"analyst"),x.status=questions.length?"questions":"drafting"});return updateBreakdownState(ctx.bp,ctx.id,s=>{addBreakdownActivity(s,"analyst",`Read the document: found ${epics} ${epics===1?"epic":"epics"} and ${features} ${features===1?"feature":"features"}${decided?`, and decided ${decided} small ${decided===1?"thing":"things"} itself`:""}`),questions.length&&addBreakdownActivity(s,"analyst",`Has ${questions.length} ${questions.length===1?"question":"questions"} for you`),s.job=questions.length?{step:"read",status:"done",found:{epics,features,stories:0},at:{done:of,of,unit}}:{step:"draft",status:"running",startedAt:now(),now:"Writing the outline: epics and features",found:{epics,features,stories:0}}}),after.status==="drafting"}async function replyStep(ctx,b,job){let q4=b.questions.find(x=>x.id===job.question),done=()=>updateJob(ctx.bp,ctx.id,j=>Object.assign(j,{status:"done",now:void 0}));if(!q4)return done(),!1;let said=[...q4.thread??[]].reverse().find(m=>m.who==="you")?.text??"",session=readBreakdownState(ctx.bp,ctx.id).sessions?.read,context=session?"":`You are the Analyst who read the business requirements document below and asked the person questions about it.
1703
1704
 
1704
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(`
1705
- `)}`});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",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(`
1706
- `))}).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)},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(`
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}):
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(`
1707
1709
 
1708
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}
1709
1711
  ${found2.q.text}`);back+=1}if(tooMany())throw tooMany()}else{if(back>=2)throw new StepError(`The ${who4} kept asking a technical question, which squad-kit does not pass on to you, so it stopped this step. Press Try again.`,`${found2.verdict.why}
@@ -1726,7 +1728,8 @@ ${specPrompt(t0,change,!1,others)}`,permissions:["read"],schema:SPEC_SCHEMA,time
1726
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}:
1727
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}
1728
1730
 
1729
- ${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);back.length&&this.log("info",`The setup command changed ${back.join(", ")}; squad-kit put ${back.length===1?"it":"them"} back`,id2)}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 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.
1730
1733
 
1731
1734
  `:sofar.length?`${intro}
1732
1735
 
@@ -1742,7 +1745,7 @@ ${prompt}`);let cwd=workFolder(t),started=Date.now(),current=-1,reached=n=>{n<=c
1742
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}
1743
1746
 
1744
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(`
1745
- `)[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 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=[],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 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}):
1746
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}):
1747
1750
  ${tail(f.output,30)}`).join(`
1748
1751
 
@@ -1832,7 +1835,7 @@ body.dark ul{background:#0004}
1832
1835
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1833
1836
 
1834
1837
  `),proof:()=>proofPrompt(meta,t,"after",ctx)},agent=readAgent(bp,role);return{ticket:{id:t.id,title:t.title},prompts:STEPS[role].map(s=>({step:s.id,name:s.name,prompt:text3[s.id]?.()??"",team:agentInstructions(agent,s.id)}))}}import fs144 from"fs";import path136 from"path";import yaml20 from"js-yaml";var MAX_FOLDER_FILES=200,MAX_FOLDER_FILE_BYTES=2e5,DEEPEST=6,NOT_LOOKED_IN=new Set(["node_modules",".git","dist","build","out","vendor","target","coverage",".next",".venv","venv","__pycache__",".ssh",".aws",".gnupg",".azure",".kube",".docker"]),HOLDS_KEYS=/^\.env|\.(pem|key|p12|pfx|jks|keystore|crt|cer|gpg)$|^id_(rsa|dsa|ecdsa|ed25519)|secret|credential|password|token|\.npmrc$|\.netrc$/i,OURS="squad-kit already does this, and two processes would fight.",OUR_HEADING=/\b(contract|workflow|code review|review process|reviews?|release|deploy|commit|push|pull request|merge request|branch|merging)\b/i,OUR_STEP=/\b(git (commit|push|merge|rebase|checkout)|gh pr|open (a|your( own)?) (pull request|pr)\b|create (a|your( own)?) (pull request|pr)\b)/i,OUR_SCRIPT=/^(pre|post)?(release|publish|deploy|commit|push|version)\b/i,NEVER_A_GATE=/^(pre|post)?(install|prepare|start|dev|serve|watch|preview)\b/i;function textOf(file3){if(fs144.statSync(file3).size>MAX_FOLDER_FILE_BYTES)return null;let data=fs144.readFileSync(file3);if(data.includes(0))return null;let text3=data.toString("utf8");return text3.includes("\uFFFD")?null:text3}function sectionsOf2(text3,title2){let out18=[],at3={name:title2,body:""};for(let line3 of text3.split(/\r?\n/)){let h=/^#{1,6}\s+(.+?)\s*#*$/.exec(line3);h?(at3.body.trim()&&out18.push(at3),at3={name:h[1],body:""}):at3.body+=`${line3}
1835
- `}return at3.body.trim()&&out18.push(at3),out18}var boxesOf=body10=>[...body10.matchAll(/^\s*[-*]\s+\[[ xX]\]\s+(.+)$/gm)].map(m=>m[1].trim()),namedFrom=file3=>path136.basename(file3).replace(/\.[^.]+$/,"");function fromMarkdown(source,text3,asRules){return sectionsOf2(text3,namedFrom(source)).flatMap((s,i)=>{let key=`${source}#${i}`,name=s.name.slice(0,MAX_TYPED_CHARS).trim(),body10=scrubSecrets(s.body.trim()),boxes=boxesOf(s.body),ours=OUR_HEADING.test(s.name)||OUR_STEP.test(body10)?{out:OURS}:{};if(boxes.length>=2){let questions=boxes.slice(0,MAX_CHECKLIST_QUESTIONS).map(q4=>scrubSecrets(q4).slice(0,MAX_TYPED_CHARS));return[{key,kind:"checklist",name,source,questions,...ours}]}if(!asRules)return[];let tooLong=body10.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters). You can add a shorter one by hand.`}:{};return[{key,kind:"rule",name,source,text:body10,...tooLong,...ours}]})}function fromOurFormat(source,text3){let doc;try{doc=yaml20.load(text3)??{}}catch{return[]}return Array.isArray(doc.additions)?doc.additions.flatMap((a,i)=>{let name=typeof a?.name=="string"?a.name.trim().slice(0,MAX_TYPED_CHARS):"",key=`${source}#${i}`;return name?a.kind==="rule"&&typeof a.text=="string"?[{key,kind:"rule",name,source,text:scrubSecrets(a.text),...a.text.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters).`}:{}}]:a.kind==="checklist"&&Array.isArray(a.questions)?[{key,kind:"checklist",name,source,questions:a.questions.filter(q4=>typeof q4=="string").slice(0,MAX_CHECKLIST_QUESTIONS).map(q4=>scrubSecrets(q4))}]:a.kind==="gate"&&typeof a.command=="string"?[{key,kind:"gate",name,source,command:a.command.trim(),when:["checks","merge","release"].includes(a.when)?a.when:"checks"}]:[]:[]}):[]}function runnerFor(root){return fs144.existsSync(path136.join(root,"pnpm-lock.yaml"))?s=>`pnpm run ${s}`:fs144.existsSync(path136.join(root,"yarn.lock"))?s=>`yarn ${s}`:s=>`npm run ${s}`}function fromPackage(root,source,text3,commands){let scripts;try{scripts=JSON.parse(text3).scripts??{}}catch{return[]}let run6=runnerFor(root),own2=[commands.test,commands.lint,commands.build].filter(Boolean).join(" ");return Object.keys(scripts).filter(s=>!NEVER_A_GATE.test(s)).map(s=>({key:`${source}#${s}`,kind:"gate",name:s,source,command:run6(s),when:"checks",...OUR_SCRIPT.test(s)?{out:OURS}:new RegExp(`\\b${s.replace(/[^\w:-]/g,"")}\\b`).test(own2)?{out:"squad-kit already runs this with the checks."}:{}}))}function lookInFolder(folder,have,commands){let root;try{if(root=fs144.realpathSync(path136.resolve(folder)),!fs144.statSync(root).isDirectory())throw new Error("not a folder")}catch{throw new Error("squad-kit can't open that folder. Check the path and try again.")}let finds=[],left=0,seen=0,looked3=0,more3=!1,walk3=(dir,depth)=>{let entries;try{entries=fs144.readdirSync(dir,{withFileTypes:!0})}catch{left+=1;return}for(let e of entries.sort((a,b)=>a.name.localeCompare(b.name))){if((looked3+=1)>MAX_FOLDER_FILES*50&&(more3=!0),more3)return;let at3=path136.join(dir,e.name),source=path136.relative(root,at3).split(path136.sep).join("/");if(HOLDS_KEYS.test(e.name)||NOT_LOOKED_IN.has(e.name)){left+=1;continue}let real2=at3;if(e.isSymbolicLink()){try{real2=fs144.realpathSync(at3)}catch{real2=""}if(!real2||!`${real2}${path136.sep}`.startsWith(`${root}${path136.sep}`)){left+=1;continue}}let stat;try{stat=fs144.statSync(real2)}catch{left+=1;continue}if(stat.isDirectory()){depth<DEEPEST&&!e.isSymbolicLink()&&walk3(at3,depth+1);continue}if(!stat.isFile())continue;let isScriptCheck=source==="scripts/check",isMarkdown=/\.(md|mdc|markdown)$/i.test(e.name),isOurs=source==="harness.yaml"||source===".squad/harness.yaml";if(!isMarkdown&&!isOurs&&!isScriptCheck&&source!=="package.json"&&!(/(^|\/)rules\//.test(source)&&/\.txt$/i.test(e.name)))continue;if((seen+=1)>MAX_FOLDER_FILES){more3=!0;return}let text3=null;try{text3=textOf(real2)}catch{}if(text3===null){left+=1;continue}isOurs?finds.push(...fromOurFormat(source,text3)):source==="package.json"?finds.push(...fromPackage(root,source,text3,commands)):isScriptCheck?finds.push({key:source,kind:"gate",name:"scripts/check",source,command:"./scripts/check",when:"checks"}):finds.push(...fromMarkdown(source,text3,/^(AGENTS|CLAUDE)\.md$/.test(e.name)||/(^|\/)rules\//.test(source)))}};walk3(root,0);let named=new Set(have.map(a=>`${a.kind}\0${a.name.trim().toLowerCase()}`));return{name:path136.basename(root),path:root,finds:finds.map(f=>named.has(`${f.kind}\0${f.name.trim().toLowerCase()}`)?{...f,already:!0}:f).slice(0,MAX_FOLDER_FILES),left,more:more3}}function additionOf(f,pick2){return f.kind==="rule"?{kind:"rule",name:f.name,agents:[pick2.agent||"all"],text:f.text??""}:f.kind==="checklist"?{kind:"checklist",name:f.name,agent:pick2.agent==="tester"?"tester":"reviewer",questions:f.questions??[]}:{kind:"gate",name:f.name,command:f.command??"",when:pick2.when??f.when??"checks",on_fail:pick2.on_fail??"stop",...pick2.part?{part:pick2.part}:{}}}var SEEN_DAYS=30,WEEK_DAYS=7,ONE_DAY=864e5,STOP_LINES={checks:/^Checks failed: /,weakened:/^Stopped before the checks: the tests were made weaker/,"no-tests":/^Not checked: .*Asked you once/,guard:/^A check (needed a file outside|tried to use Docker)/,"test-change":/^squad-kit keeps the current way of testing/,review:/^(Asked for \d+ fix|Still asks for )/,proof:/^(Could not take the screenshots|No screenshots yet)/,"bad-proof":/^The screenshots did not pass squad-kit's check/,"not-shown":/^(The screenshots do not show .* clearly yet|Could not show .* on a screen after)/,broken:/^(The screenshots show .* not working yet|Still not working after)/,window:/^The \w+ asked .*squad-kit refused\.$/,questions:/did not bother you with it\.$|^The \w+ asked a technical question/,sandbox:/^(Reaching|Reading|Writing) .* was blocked: |^A download from \S+ was not allowed for this project/,lane:/^The (Builder|Tester|Reviewer|project's checks) changed .*squad-kit put |^Stopped the \w+: /,hook:/^The project's own check said no/,merge:/^(Could not merge into |It clashes with newer changes)|clashes with newer changes in \S+, so it left/,cap:/^Paused: /,budget:/^Paused before the next step/},PASSED={checks:t=>t.gates?.checks==="pass"?lineAt(t,/^All checks pass/):void 0,"test-change":t=>lineAt(t,/^squad-kit keeps the new way of testing/),review:t=>t.gates?.review==="pass"?lineAt(t,/^Approved: /):void 0,proof:t=>t.gates?.proof==="pass"&&t.proof?.length?t.history?.filter(h=>h.step==="proof").at(-1)?.to??t.activity[0]?.at:void 0,merge:t=>t.mergedAt},DID_LINES={spec:[/^Approved the spec/,"You approved"],plan:[/^Approved the (design|plan)/,"You approved"],"looks-right":[/^Checked the proof\. Looks right/,"You accepted"],ask:[/^Asked you: /,"Asked on"],chat:[/^Started talking it through/,"You talked on"],decisions:[/\. Decision saved as /,"Saved decisions on"],depends:[/so the build waits for you to choose/,"Waited for you on"],change:[/^Approved change \S+ \(big\)/,"You approved a big change on"],"reviewer-floor":[/as strong as the Builder\)$/,"Raised the model on"],"also-changed":[/^Also changed, not in the plan/,"Named something on"],"off-screen":[/^"Done when" \d+ is not on a screen/,"Marked on"],"api-proof":[/^squad-kit asked .* and kept the answer/,"Used on"],"take-over":[/^Took it over/,"You took over"],"pull-request":[/^Opened pull request/,"Opened on"],checkout:[/^It goes in once your own copy/,"Waited on"],release:[/^Released in /,"Sent out"],watchdog:[/^Looks stuck: /,"Flagged"],"step-up":[/^Runs on .*after \d+ failed tr/,"Moved up on"],"plain-words":[/^The Tester wrote something technical/,"Kept words out of the proof on"],interrupted:[/^Carried on.* after (a restart|the computer slept)|^The computer slept|^Resumed after sleep/,"Carried on after it on"],blips:[/^The connection or the AI service had a problem/,"Waited on"],leftovers:[/^Stopped (a program|\d+ programs) left running/,"Stopped programs on"]},HIT_LINES={MAX_CHECK_TRIES:/^Checks failed: .*\(try (\d+) of \1\)$/,MAX_REVIEW_ROUNDS:/^Still asks for /,MAX_RETAKES:/^Could not show .* on a screen after \d+ more tries/,MAX_PROOF_ROUNDS:/^Still not working after /,MAX_BASE_TRIES:/came in \d+ times, so it waits for you$/,REFUSALS_TO_STOP:/^Stopped: .*kept asking for things it may not do/,MISTAKES_TO_STOP:/^Stopped: .*kept using squad-kit's tools the wrong way/,TECHNICAL_TRIES:/^Stopped: .*kept asking a technical question/,BEFORE_SHOTS_MS:/^Stopped the before screenshots after/},WAIT_ROW={questions:"questions",spec:"spec",plan:"plan",proof:"looks-right",release:"release",ask:"ask",yours:"take-over",chat:"chat",change:"change",depends:"depends"},lineAt=(t,re)=>t.activity.find(a=>re.test(a.text))?.at,stopRow=s=>s.kind??s.gate??(s.budget?"budget":"ways-out"),overrideRow=o=>o.added?"gate-cant-run":o.weakened?.length?"weakened":o.gate==="hooks"?"hook":o.gate;function needRow(t){let gate=t.waiting?.kind==="stuck"?t.stuck?.added?.find(g=>g.id!==HARNESS_FILE_GATE):void 0;return gate?`add:${gate.id}`:t.waiting?t.waiting.kind==="stuck"?t.stuck?stopRow(t.stuck):"ways-out":WAIT_ROW[t.waiting.kind]:t.quiet?.kind==="stuck"?"watchdog":t.outdated?.length?"decisions":t.testChange?.ask?"test-change":"change"}var storyWord=n=>`${n} ${n===1?"story":"stories"}`;function harnessSeen(bp,meta,now2=Date.now()){let from=now2-SEEN_DAYS*ONE_DAY,week=now2-WEEK_DAYS*ONE_DAY,fresh=at3=>!!at3&&Date.parse(at3)>=from,rows2={},weekIds={},hits={},did={},said={},tickets=listTickets(bp).filter(t=>!t.deleting),counted=0,wentPast=0;for(let t of tickets){let lines3=t.activity.filter(a=>fresh(a.at));(lines3.length||fresh(t.createdAt))&&(counted+=1);let stops=new Map,stop=(row3,at3)=>{fresh(at3)&&!(stops.get(row3)>at3)&&stops.set(row3,at3)};for(let[row3,re]of Object.entries(STOP_LINES))stop(row3,lines3.find(a=>re.test(a.text))?.at);t.stuck&&t.waiting?.kind==="stuck"&&stopRow(t.stuck)!=="ways-out"&&stop(stopRow(t.stuck),t.activity[0]?.at??t.createdAt);for(let o of t.overrides??[])stop(overrideRow(o),o.at);for(let r of t.added??[])r.stopped&&stop(`add:${r.id}`,r.stopped);t.overrides?.some(o=>fresh(o.at))&&(wentPast+=1);for(let[row3,at3]of stops){let r=rows2[row3]??={};r.stopped=(r.stopped??0)+1,Date.parse(at3)>=week&&(r.week=(r.week??0)+1,(weekIds[row3]??=[]).push(t.id)),(!r.last||at3>r.last.at)&&(r.last={id:t.id,title:t.title,at:at3})}for(let[row3,at3]of Object.entries(PASSED).map(([row4,of])=>[row4,of(t)]))!stops.has(row3)&&fresh(at3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1);for(let r of(t.added??[]).filter(x=>fresh(x.at))){let row3=`add:${r.id}`,s=said[row3]??={would:0,warned:0},no=r.answers?.some(a=>a.answer==="no");(r.result==="would-stop"||no&&r.state==="try")&&(s.would+=1),r.result==="warned"&&(s.warned+=1),r.result==="cant-run"&&!(s.cant?.at>r.at)&&(s.cant={id:t.id,title:t.title,at:r.at}),(r.result==="passed"||r.answers?.length&&r.answers.every(a=>a.answer==="yes"))&&!stops.has(row3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1)}for(let[row3,[re]]of Object.entries(DID_LINES))lines3.some(a=>re.test(a.text))&&(did[row3]=(did[row3]??0)+1);for(let[limit,re]of Object.entries(HIT_LINES)){let at3=lines3.find(a=>re.test(a.text))?.at;at3&&!(hits[limit]?.at>at3)&&(hits[limit]={id:t.id,title:t.title,at:at3})}}for(let row3 of Object.keys(PASSED))rows2[row3]&&(rows2[row3]={passed:0,stopped:0,...rows2[row3]});for(let[row3,s]of Object.entries(said)){let words2=[s.would?`Would have stopped ${storyWord(s.would)}`:"",s.warned?`Warned on ${storyWord(s.warned)}`:"",s.cant?`Could not run on ${s.cant.id}`:""].filter(Boolean);rows2[row3]={passed:0,stopped:0,...rows2[row3],...words2.length?{did:words2.join(". ")}:{}}}for(let[row3,n]of Object.entries(did))(rows2[row3]??={}).did=`${DID_LINES[row3][1]} ${storyWord(n)}`;wentPast&&((rows2["ways-out"]??={}).did=`You went past a check on ${storyWord(wentPast)}`);let pause2=readPause();pause2&&pause2.project===meta.project&&fresh(pause2.since)&&((rows2["usage-limit"]??={}).did=`Last waited ${pause2.since.slice(0,10)}`);let needs=inbox(bp).filter(t=>!t.deleting&&!t.archived).map(t=>({id:t.id,summary:t.waiting?.summary??t.quiet?.detail??t.status,row:needRow(t),proof:t.waiting?.kind==="proof"}));return{stories:counted,rows:rows2,weekIds,hits,needs}}function lineStages(){let you=sub=>({kind:"you",sub}),squad=sub=>({kind:"squad",sub}),skip=sub=>({kind:"skip",sub}),on=(full,express=full,auto=full)=>({full,express,auto}),stage=(id2,name,about,when2,gates)=>({id:id2,name,about,on:when2,gates}),g=(id2,name,what,fail,more3)=>({id:id2,name,what,...fail?{fail}:{},...more3}),y=(id2,name,what,fail,more3)=>g(id2,name,what,fail,{you:!0,...more3}),ON_ITS_OWN=["express","auto"],floor=QUALITIES.filter(q4=>QUALITY_PRESETS[q4].reviewerFloor).map(q4=>q4.charAt(0).toUpperCase()+q4.slice(1)).join(" and ");return[stage("setup","Setup","Once for each project, before any story.",on(you("You confirm")),[g("tool-ready","Your AI tool is here","It must be installed, or nothing runs.","It says what to do."),g("first-run","A first run on untouched code","Your tests and app run once in squad-kit's own copy. What already fails is noted, so the Builder leaves other screens alone.","The setup goes on. A story must still pass its checks."),y("overview","You confirm what it found","The Architect explains your project in plain words.","You say what is wrong, and it reads again."),g("project-rules","Your project's own rules","Every agent first reads your rule files and your team's wiki. squad-kit runs the process, and those files cover the code.")]),stage("breakdown","Breakdown","Your requirements become a tree of stories. Skip it if you start from one story.",on({...skip("Optional"),off:!0},{...skip("Optional"),off:!0},you("You approve")),[g("brd-questions","Only real gaps and clashes",`At most ${MAX_QUESTIONS} questions on a document, where it leaves a gap or disagrees with itself.`,"A technical one goes back to the agent."),y("brd-approve","You approve the breakdown",'Nothing is built or sent to your tracker before that. Every story needs its "done when".',"Approve is refused and says what to fix."),g("brd-queue","Stories start in order","The next starts when what it needs is merged and a place is free.","Your budget, your AI limit or two stopped stories hold the queue, with a reason."),g("first-story","A project with no code starts with one story","squad-kit adds Set up the product: it picks the tools and makes a first version. Nothing else starts until it merges, so every story builds on the same start.","Another ticket waits, and says which one it waits for.")]),stage("story","Story","The Analyst reads your story and asks only what it leaves open.",on(you("You answer"),skip("No questions"),skip("Approved before")),[g("questions","Only business questions reach you","The Analyst asks about gaps and clashes. Asks about settings, secrets or code history are refused before you see them.",void 0,{skip:ON_ITS_OWN}),y("spec","You approve what gets built",'The story and its "done when" list wait for you.',"You ask for a change and it is rewritten.",{skip:ON_ITS_OWN,note:{auto:"Approved with the breakdown."}}),y("ask","An agent can stop and ask you","The Architect, Builder or Reviewer can ask one plain question. It passes the same filter.",void 0,{note:{auto:"Only for a critical gap or a clash. Small choices go under Decided for you."}}),y("chat","Talk it through","With any agent that worked on the story. It waits while you talk.")]),stage("plan","Plan","The Architect reads your project and writes the plan in plain words.",on(you("You approve"),skip("On its own"),skip("On its own")),[y("plan","You approve the plan","Nothing is built before you approve how.","You ask for a change and it is rewritten.",{skip:ON_ITS_OWN}),g("decisions","Saved decisions stay rules","Each plan says which ones it follows or replaces."),y("depends","Stories that depend on each other","A story that needs unmerged work waits: build on it, build anyway, or wait."),y("change","A big change comes back to you","The Builder pauses, and the updated design waits for your yes.")]),stage("build","Build","The Builder writes the code in small steps, in a locked room.",on(squad("Builder")),[g("sandbox","The guard","Each story has its own copy. No agent can read your keys or sign-ins. The Builder and Tester can't read .env files either, and reach only the websites the project's network level allows.","Blocked, with a line on the story. A website it may not reach stops nothing: the work goes on without it."),g("guard","A second guard for your project","Your tests, app, setup and release run locked in too, with keys hidden and Docker closed.","A card: an engineer allows the file, or the plan changes."),g("lane","Files no agent can change","squad-kit's folder, AI instruction files, commit hooks, files an engineer locked. No commit or branch change, and a push is refused.","A file goes back, with a line. A moved branch stops the step."),g("local-files","Test values, not your own","A file marked local gets values kept for stories. No agent can read it."),y("new-tests","New tests",`The Builder writes tests for every "done when". On a new story you can turn off Write new tests: your project's own checks, the review and the proof still run, and the story says "Built without new tests". An engineer can keep it on for every story.`,void 0,{note:{auto:"Always on: a story from the breakdown writes its tests."}})]),stage("checks","Checks","squad-kit runs your project's own checks. No agent is asked if they passed.",on(squad("squad-kit")),[g("checks","Your project's own checks","Test, lint and build, plus scripts/check when your project has one. The Builder's word doesn't count. A failed check runs once more, so a flaky test doesn't stop the story. For the tests, the failed ones run on their own first: one that still fails fails the check at once, and if they pass, all the tests run once more and decide.",`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`),g("old-tests","An old bug is not the story's","A failed test in a file the story didn't change runs on its own on main's code. If it fails there too, it is an old bug: the story goes on, the checks say so, and squad-kit makes one new story to fix that test, which waits on the board until you start it. A story only goes past a test that main already fails.","If it passes on main, the Builder fixes it, as for any failed check."),g("weakened","Weakened tests","Tests deleted, switched off or reduced, test settings changed, or a change to what a must-pass gate of your team runs.","It stops. Only you can go past it, with a reason on the record."),g("no-tests","No tests, no green","No tests never means a green pass. Lint, build and the app starting must still pass.","Asked once per project: add tests, or go on without."),g("test-change","Test commands stay strong","A new test command stays only if it passes and runs at least as many tests.","If squad-kit can't tell, you choose."),g("gate-cant-run","A check that can't run","One of your team's gates that can't run, or a file of additions that can't be read, says nothing about the code. It never counts against the story, and it is never dropped without a word.","It retries by itself, then a card: try again, let an engineer look, or go on with a reason. On Builds by itself it waits for a person.")]),stage("review","Review","A Reviewer that never saw the build reads the Builder's work.",on(squad("Reviewer")),[g("review","A fresh Reviewer",'It checks every "done when" and saved decision, and looks for bugs, security holes and missing or weakened tests. It changes nothing.',`Back to the Builder, up to ${MAX_REVIEW_ROUNDS} rounds. Then you decide.`),g("reviewer-floor","Never weaker than the Builder",`On ${floor}, its model is at least as strong.`),g("also-changed","Also changed","It names what the change touches beyond the story and plan.","It doesn't block. It shows at Your check.")]),stage("proof","Proof",'The Tester runs your real app and shows each "done when" working.',on(squad("Tester")),[g("proof","Pictures of the real app",`Up to ${6} for each "done when".`,"None: a card, and you decide."),g("bad-proof","squad-kit checks each picture",`Real, at least ${MIN_SIZE.width} by ${MIN_SIZE.height}, not blank, taken in this step.`,"Back to the Tester once. Then a card."),g("not-shown",'A second look at every "done when"',"A separate run judges its pictures: met, not working, not shown, or not on a screen.",`Not shown: up to ${2} retakes. Then a card.`),g("broken","Not working yet","Pictures that show it does not work never pass.",`Back to the Builder ${"once"}. Then a card.`),g("off-screen","Not on a screen",'Takes two voices: the Tester and the check. If every "done when" ends that way, you decide.'),g("api-proof","Answers from your API","Work with no screen can show a saved answer from this story's own API."),g("window","Fixed tools only","The Tester uses squad-kit's own tools, on this story's app only.",`Refused. ${REFUSALS_TO_STOP} refusals stop it.`)]),stage("yours","Your check","You look at the proof, try the app, and ask anything.",on(you("You decide"),you("You decide"),skip("On its own")),[y("looks-right","You look and decide","Where to look, Try it yourself, Ask a question, Something's wrong. Nothing merges before Looks right.","Something's wrong sends it back with your words.",{skip:["auto"],note:{auto:"It merges when the checks, review and proof pass. If not, it comes to you."}}),y("take-over","Take it over","Work in the story's copy yourself. The squad keeps off until you give it back."),y("outside","Work in another AI tool","Copy a prompt, and a conversation in any AI tool carries on from this step. The squad keeps off until it comes back. It keeps a label on its record, and Check runs squad-kit's own checks, review and proof on it. What is spent there is yours, and the guard and the limits do not apply there."),g("accept-order","First things first","A story built on another is accepted after it, and an open change is settled first.","Accept says what comes first.")]),stage("merge","Merge","The story joins the rest of your code.",on(squad("squad-kit")),[g("hook","Your check on every save","Your commit hooks run on every save, inside the guard.","The Builder fixes it, or you save without it and say why."),g("merge","A clean merge","The latest code comes in first. A clash is never forced.",`The newest code comes in by itself, up to ${MAX_BASE_TRIES} times. Then a card.`),g("pull-request","Pull requests",`squad-kit opens and merges them. Waiting for an approver is a normal wait. One merged on the code host is noticed within ${OUTSIDE_LOOK_MS/6e4} minutes, or at once with Check again.`,"Try again, or let the release merge the work."),g("checkout","Your own copy is safe","With changes in the same files, the merge waits. It never overwrites them.")]),stage("release","Release","You choose what goes out and when. Every track stops here.",on(you("You decide")),[y("release","Release stays yours","Deploying never happens by itself.","If it can't be sent, it isn't marked as sent. Send again finishes it."),y("leave-out","Leave a story out","It stays in your code and goes out with the next release."),g("release-clash","Nothing moves on a clash","Every merge is tried first. What the others rely on goes first.","A clash: one card, nothing moved. A failed part: the ones after it wait."),g("release-lock","One release at a time","One cut off by a restart is finished or rolled back on the next start.")])]}function alwaysOnRows(){let a=(id2,name,what,then)=>({id:id2,name,what,...then?{then}:{}});return[a("watchdog","Nothing hangs in silence","When a step goes quiet, the watchdog looks at what is really running, to tell busy from stuck.","You get a card with ways out."),a("usage-limit","Your AI limit is respected","At the limit every project waits together, then carries on from the same step.","Never a failed try, and you can end the wait yourself."),a("step-up","A stronger model after a failed try","The Builder moves up, unless someone chose an exact model.",`On Thrifty, after ${QUALITY_PRESETS.thrifty.stepUp.after} failed tries.`),a("cap","A run can't go on forever","Each run has a most it can use. See Limits.","It pauses with its work kept. Give it more doubles it."),a("budget","Your budget",`If you pay per use, a story and a month can have one. At ${WARN_AT*100}% the board says so.`,"At 100% the story or the Analyst stops between two steps, never mid-step."),a("ways-out","Every stop has a way out","Try again, do it yourself, go around it, or stop. Only you can go past a failed check.","Your reason stays on the story, the pull request and the release notes."),a("plain-words","Plain words only","Raw errors never reach you.","The exact error goes to the engineer details and the log."),a("interrupted","An interruption is not a failure","A restart, a Stop or a sleeping computer never counts as a failed try.","The step carries on where it was."),a("blips","A lost connection is not a failure","The step runs again by itself.","If it does not pass, you get a card."),a("leftovers","Nothing left behind","A program a step leaves running is stopped. Programs of your own are never touched.","On Windows it stops only what it started itself."),a("product-lock","One story runs the product at a time","Stories that need the whole product take turns, and say who they wait for."),a("room-for-copy","Room on the disk","Before a first copy of the code, squad-kit compares the free space with the size of the project.","Too little: a card says how much it needs and how much is free. Free up some space, then press Try again."),a("save-trouble","A full disk holds the work","If squad-kit can't save its files, the project waits and says so on every page.","It carries on once saving works."),a("board-lock","Your board is locked","It opens only on your own computer's address, with your link's key.","A request from any other website is refused."),a("report","Reports leave your secrets out",NEVER)]}function limitRows(meta){let row3=(group,show3=String)=>(limit,name,what="")=>{let[id2,n]=Object.entries(limit)[0];return{id:id2,group,n:show3(n),name,what}},tries=row3("tries"),pics=row3("pictures"),asks=row3("asks"),time2=row3("time",ms2=>`${ms2/6e4} min`),size2=row3("sizes"),bytes=row3("sizes",b=>`${b/1024/1024} MB`);return[tries({MAX_CHECK_TRIES},"Tries at the checks","Then a card."),tries({MAX_RUNS_ON_MAIN},"Runs of a failed test on main's code","It stops at the first that fails there too."),size2({MAX_ALONE},"Failed tests run on their own first","With more, all the tests run again at once."),size2({MAX_OLD},"Failed tests run on main's code","With more, the Builder fixes them, as before."),tries({MAX_REVIEW_ROUNDS},"Review rounds","Then you decide."),tries({MAX_RETAKES:2},"Retakes",`For a "done when" the pictures don't show clearly.`),tries({MAX_PROOF_ROUNDS:1},"Returns after a broken picture","To the Builder. Then you decide."),tries({MAX_BASE_TRIES},"Clash retries","While the code keeps moving."),tries({BLIP_WAITS_MIN:BLIP_WAITS_MIN.length},"Waits after a lost connection",`After ${BLIP_WAITS_MIN.join(", ")} minutes. Then a card.`),pics({MAX_PICTURES_PER_DONE_WHEN:6},'Pictures for each "done when"',"So each part can show."),pics({MAX_PICTURES:40},"Pictures in one proof step","Counting each one taken again."),pics({MAX_SHOTS_PER_CALL},"Pictures in one call","More has to be split over calls."),pics({MAX_API_CALLS},"Calls to your API in one proof","Only this story's own app."),asks({REFUSALS_TO_STOP},"Refusals","Forbidden asks, then the agent is stopped."),asks({MISTAKES_TO_STOP},"Calls in the wrong shape","Harmless, but past this the step ends."),asks({MAX_STUCK},"Times a run can say it is stuck","After that, squad-kit says it already tried."),asks({TECHNICAL_TRIES:2},"Tries for a technical question","Back to its agent, never to you. Then it stops."),asks({MAX_QUESTIONS},"Questions on one document","Only real gaps and clashes."),asks({MAX_DECIDED},"Small choices in one answer","Logged under Decided for you."),time2({WORDS_STEP_MS},"Analyst and Architect","One run of words: questions, a spec, a design."),time2({BUILD_STEP_MS},"Builder","One run of building or fixing."),time2({REVIEW_STEP_MS},"Reviewer","One round, or one look at the pictures."),time2({PROOF_STEP_MS},"Tester"),time2({BRD_READ_MS},"Reading a document","A long document gets twice its usual read."),time2({LONGEST_READ_MS},"Reading a very long document","The most, however long it is."),time2({BRD_WRITE_MS},"Writing a breakdown"),time2({CHECK_MS},"One check"),time2({CHECK_SILENT_MS},"A silent check","It prints nothing for this long."),time2({APP_WAIT_MS},"Waiting for your app"),time2({TURN_WAIT_MS},"Tests and your app in one copy","One waits for the other, then it stops and says so."),time2({KEPT_APP_MS},"Your app kept for the proof","After the review, while nothing uses it. Then it stops."),time2({SETUP_MS},"Setting up your app"),time2({DOCKER_WAIT_MS},"Starting in Docker"),time2({BEFORE_SHOTS_MS},"Before pictures","Beside the build, holding nothing up."),time2({ON_MAIN_MS},"Failed tests on main's code","All their runs together. Then the Builder fixes them, as before."),time2({HOOK_MS},"Your check on a save"),time2({NETWORK_GIT_MS},"Reaching your code host","One push, pull or fetch."),time2({PIPELINE_MS},"Your after-release step","Inside the guard, after a release."),time2({MAX_LANDING_WAIT_MS},"Waiting for a story to merge","Before the newest code comes in."),time2({OUTSIDE_LOOK_MS},"Looking for merges on your code host","Pull requests merged there, outside squad-kit. Check again looks now."),time2({WAIT_LIMIT_MS},"Waiting for the product"),time2({TRY_YIELDS_AFTER_MS},"Trying a story yourself","Once another story needs it."),time2({QUIET_MS},"Quiet before a look","Then the watchdog looks."),time2({LONG_MS},"Busy before Taking long","Working, with no progress."),time2({RETRY_MS},"Looking again at your AI limit","When your AI tool doesn't say when it ends."),size2({parallel:parallelLimit(meta?.parallel)},"Stories at once","Across your projects. Change it in Settings."),size2({MAX_PARTS},"Parts of one product"),size2({MAX_UPLOADS},"Files on one story"),size2({MAX_RULES},"Rules your team can add","Remove one to add another."),size2({MAX_RULE_CHARS},"Characters in one rule","Agents read every rule at every step, so each stays short."),size2({MAX_CHECKLIST_QUESTIONS},"Questions on one checklist"),size2({MAX_TYPED_CHARS},"Characters in a name, a question or a command"),size2({MAX_FOLDER_FILES},"Files looked at in a folder you bring in","The rest are left out, and the sheet says so."),row3("sizes",b=>`${b/1e3} KB`)({MAX_FOLDER_FILE_BYTES},"Largest file read in that folder","A bigger one is left out."),bytes({MAX_UPLOAD_BYTES},"One file"),bytes({MAX_NOTE_BYTES},"One voice or video note")]}var HARNESS_CAP_NAME={analyst:"Analyst",architect:"Architect",builder:"Builder","builder.fix":"Builder, fixes",reviewer:"Reviewer",tester:"Tester",chat:"Talking a story through"},HARNESS_PRIVATE_NAME={".ssh":"SSH keys",".aws":"AWS sign-in",".gnupg":"GPG keys",".config/gh":"GitHub sign-in",".azure":"Azure sign-in",".kube":"Kubernetes sign-in",".docker/config.json":"Docker sign-in",".netrc":"Saved website passwords",".git-credentials":"Saved git passwords"},HARNESS_REFUSED={settings:"Change a squad-kit setting or a protected file",secret:"Hand over a password or a key",history:"Change the history of the code"},HARNESS_TOOL_NAME={take_screenshots:"Take pictures",look_at_page:"Look at a page",start_app:"Start the app",stop_app:"Stop the app",app_status:"See if the app runs",run_checks:"Run the project's checks",add_part:"Add a part of the product",ask_api:"Ask your API",i_am_stuck:"Say it is stuck"};function levelSites(level){return level==="off"?["Only this computer"]:level==="standard"?["Any website but the closed ones"]:RECOMMENDED_GROUPS.map(g=>g.name)}function agentPowers(){let yes=label3=>[label3,!0],no=label3=>[label3,!1],reads=[yes("Read your project"),no("Change code"),no("Run commands"),no("No internet")];return{analyst:{can:reads},architect:{can:reads},builder:{can:[yes("Read your project"),yes("Change code"),yes("Run commands"),yes("The websites your network level allows"),yes("squad-kit's tools: pictures, checks, the app")]},reviewer:{can:[...reads,yes("Look at a page of this story's app")]},tester:{can:[...reads,yes("squad-kit's tools: pictures, the app, your API")]}}}function rowDetails(meta,root){let min=ms2=>`${ms2/6e4} min`,repos=meta?reposOf(meta):[],run6=(...keys)=>repos.map(r=>[repos.length>1?`${r.path}: `:"",keys.map(k=>r.commands[k]).filter(Boolean).join(", ")]).filter(([,c])=>c).map(([p,c])=>`${p}${c}`).join("; ")||"Not set yet",guard=guardSupport(),tools4=sandboxStatus(),level=parseNetwork(meta?.network)??DEFAULT_NETWORK,state=(on,why)=>`${on?"On":"Off"}: ${why}`,names2=list2=>list2?.map(f=>f.path).join(", ")||"None yet",tab=name=>`The story's ${name} tab`,code=(file3,...more3)=>[...more3,["In the code",`src/${file3}`]];return{"tool-ready":code("engine/onboard.ts",["Found",meta?.tools?.map(t=>`${t.name}: ${t.ready?"ready":"not found"}`).join(", ")||"Not looked yet"]),"first-run":code("engine/onboard.ts",["Runs",run6("setup","test","start")],["See it in","The Overview page"]),overview:code("board/first-run.ts",["See it in","The Overview page"]),"gate-cant-run":code("engine/gate-run.ts",["File",".squad/harness.yaml"]),"project-rules":code("engine/repos.ts",["Reads",(root?rulesFiles(root).map(f=>f.slice(root.length+1)):[]).join(", ")||"None found yet"]),"brd-questions":code("engine/breakdown-job.ts",["Limit",`${MAX_QUESTIONS} questions`]),"brd-approve":code("board/requirements-actions.ts",["See it in","The Requirements page"]),"brd-queue":code("board/requirements-build.ts",["Places",`${parallelLimit(meta?.parallel)} at once`]),"first-story":code("board/first-story.ts"),questions:code("engine/question-filter.ts",["Refused",Object.values(HARNESS_REFUSED).join(", ")],["Tries",`${2} back to its agent`]),spec:code("board/actions.ts",["See it in",tab("Spec")]),ask:code("engine/live.ts"),chat:code("board/actions.ts"),plan:code("board/actions.ts",["See it in",tab("Design")]),decisions:code("board/actions.ts",["See it in","The Architecture page"]),depends:code("board/depends.ts"),change:code("board/actions.ts"),sandbox:code("engine/sandbox.ts",["On this computer",tools4.map(t=>state(t.available,t.why)).join(", ")||"Off"],["Network level",NETWORK_NAME[level]],["Also allowed",meta?.allowedHosts?.join(", ")||"None yet"],["Lists","src/engine/network-lists.ts"]),guard:code("engine/project-guard.ts",["On this computer",state(guard.on,guard.why)],["Network",networkPolicy(level).commands==="this-computer"?"Only this computer (Off)":"Open"],["Also allowed",[...meta?.guard?.allowRead??[],...meta?.guard?.allowWrite??[]].join(", ")||"None yet"]),lane:code("engine/lane.ts",["Locked here",names2(meta?.lockedFiles)]),"local-files":code("engine/local-files.ts",["Local here",names2(meta?.localFiles)]),"new-tests":code("engine/prompts.ts",["Here",meta?.alwaysNewTests?"Always on: every story writes new tests":"Your choice on each new story, on by default"],["Set in","Settings"]),checks:code("engine/checks.ts",["Runs",run6("test","lint","build")],["Limit",`${MAX_CHECK_TRIES} tries, ${min(CHECK_MS)} each, ${min(CHECK_SILENT_MS)} with no output`],["Decided by","The exit code, never an agent"],["See it in",tab("Developer")]),"old-tests":code("engine/old-tests.ts",["Runs",run6("test")],["Limit",`${MAX_OLD} failed tests, each up to ${MAX_RUNS_ON_MAIN} times, ${min(ON_MAIN_MS)} in all`],["Reads","Playwright, Vitest, Jest, pytest and go test"],["See it in","The proof page and the Releases page"]),weakened:code("engine/weakened.ts",["Looks at","Deleted tests, skip marks, fewer tests, test settings"],["See it in",tab("Changes")]),"no-tests":code("engine/not-checked.ts",["Here",meta?.noTests?"You chose to go on without tests":"Not asked yet"]),"test-change":code("engine/test-change.ts",["Runs",run6("test")],["Compares","Pass or fail, and how many tests ran"]),review:code("engine/live.ts",["Limit",`${MAX_REVIEW_ROUNDS} rounds, ${min(REVIEW_STEP_MS)} each`],["See it in",tab("Conversation")]),"reviewer-floor":code("engine/levels.ts"),"also-changed":code("board/also-changed.ts",["Blocks","Nothing"]),proof:code("engine/live.ts",["Runs",run6("start")],["Limit",`${6} for each, ${40} in one step, ${min(PROOF_STEP_MS)}`]),"bad-proof":code("engine/proof-check.ts"),"not-shown":code("engine/proof-verdict.ts",["Limit",`${2} retakes`]),broken:code("engine/proof-verdict.ts",["Limit",`${2} rounds`]),"off-screen":code("engine/proof-verdict.ts"),"api-proof":code("engine/window.ts",["Limit",`${MAX_API_CALLS} calls in one proof`]),window:code("engine/window.ts",["Limit",`${REFUSALS_TO_STOP} refusals, ${MISTAKES_TO_STOP} calls in the wrong shape`]),"looks-right":code("board/actions.ts",["See it in","The proof page"]),"take-over":code("board/actions.ts"),outside:code("board/outside.ts",["File",".squad/how-to-work-here.md"]),"accept-order":code("board/depends.ts"),hook:code("engine/git.ts",["Limit",min(HOOK_MS)]),merge:code("board/bring-in.ts",["Limit",`${MAX_BASE_TRIES} tries`]),"pull-request":code("engine/sync.ts",["Limit",`${min(NETWORK_GIT_MS)} to reach your code host`]),checkout:code("board/auto.ts"),release:code("engine/release.ts",["Runs",run6("deploy")],["Limit",min(PIPELINE_MS)]),"leave-out":code("engine/leave-out.ts"),"release-clash":code("engine/release.ts"),"release-lock":code("board/lock.ts"),"room-for-copy":code("engine/git.ts"),watchdog:code("engine/watchdog.ts",["Limit",`Looks after ${min(QUIET_MS)} quiet, says taking long after ${min(LONG_MS)}`]),"usage-limit":code("engine/usage-limit.ts",["Looks again",`Every ${min(RETRY_MS)} when the tool does not say when`]),"step-up":code("engine/levels.ts"),cap:code("engine/caps.ts"),budget:code("board/budget.ts",["Set in","Settings"]),"ways-out":code("board/actions.ts"),"plain-words":code("engine/plain-stop.ts"),interrupted:code("engine/interrupt.ts"),blips:code("engine/live.ts",["Waits",`${BLIP_WAITS_MIN.join(", ")} min`]),leftovers:code("engine/leftovers.ts"),"product-lock":code("engine/product-lock.ts",["Limit",min(WAIT_LIMIT_MS)]),"save-trouble":code("engine/save-trouble.ts"),"board-lock":code("console/auth.ts"),report:code("engine/report.ts")}}function trackCell(stage,g,track2){let note2=g.note?.[track2]?{note:g.note[track2]}:{};return g.skip?.includes(track2)?{word:"By itself",...note2}:stage.on[track2].off?{word:"Not used"}:{word:g.you?"You":"Runs",...note2}}var RULE_STAGE={all:"build",analyst:"story",architect:"plan",builder:"build",reviewer:"review",tester:"proof"},additionStage=a=>a.kind==="gate"?a.when:a.kind==="checklist"?a.agent==="reviewer"?"review":"proof":RULE_STAGE[a.agents.length===1?a.agents[0]:"all"];function additionRow(a){let by2=[["Added by",a.added_by],["From",a.from==="typed"?"Typed here":`The folder ${a.from}`]],team={state:a.state,kind:a.kind},row3={id:`add:${a.id}`,name:a.name,team};if(a.kind==="rule")return{...row3,what:a.text,d:by2};if(a.kind==="checklist"){let fail2=a.state==="try"?"The answers show on the story. A no stops nothing.":a.agent==="reviewer"?"A no counts as a review finding.":'A no goes the same way as a "done when" not shown.';return{...row3,what:`The ${a.agent==="reviewer"?"Reviewer":"Tester"} answers: ${a.questions.join(" ")}`,fail:fail2,d:by2}}let fail=a.state==="try"?'It is noted as "would have stopped". Nothing stops.':a.on_fail==="warn"?"It is noted. Nothing stops.":{checks:`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`,merge:"The change is not saved, and a card has the ways out.",release:"The release does not go out, and the page has the ways out."}[a.when];return{...row3,what:"A command squad-kit runs itself. No agent is asked if it passed.",fail,d:[["Command",a.command],...a.part?[["Part",a.part]]:[],...by2]}}function harnessView(meta,bp){let quality=meta?.quality??DEFAULT_QUALITY,caps=QUALITY_PRESETS[quality].caps,seen=meta&&bp?harnessSeen(bp,meta):void 0,details=rowDetails(meta,bp?.root),addedOn=new Set(bp?readHarness(bp).additions.filter(a=>a.state!=="off").map(a=>`add:${a.id}`):[]),waiting2=seen?.needs.map(n=>n.row.startsWith("add:")&&!addedOn.has(n.row)?{...n,row:"gate-cant-run"}:n),fill2=r=>{let needs=waiting2?.filter(n=>n.row===r.id);return{...r,d:details[r.id]??r.d??[],...seen?.rows[r.id]?{seen:seen.rows[r.id]}:{},...needs?.length?{needs}:{}}},file3=bp?readHarness(bp):{additions:[],log:[]},live=file3.additions.filter(a=>a.state!=="off"),addedCell=a=>({word:"Runs",...a.state==="try"?{note:"Try first"}:{}}),stages=lineStages().map(s=>{let gates=[...s.gates.map(g=>({...fill2(g),cells:{full:trackCell(s,g,"full"),express:trackCell(s,g,"express"),auto:trackCell(s,g,"auto")}})),...live.filter(a=>additionStage(a)===s.id).map(a=>({...fill2(additionRow(a)),cells:{full:addedCell(a),express:addedCell(a),auto:addedCell(a)}}))];return{...s,gates,stopped:new Set(gates.flatMap(g=>seen?.weekIds[g.id]??[])).size}}),always=alwaysOnRows().map(fill2),rows2=[...stages.flatMap(s=>s.gates),...always],offHere=(sandboxStatus().some(t=>t.available)?0:1)+(guardSupport().on?0:1),level=parseNetwork(meta?.network)??DEFAULT_NETWORK;return{quality,qualityNote:QUALITY_NOTE,range:{days:SEEN_DAYS,weekDays:WEEK_DAYS,stories:seen?.stories??0},tiles:{working:rows2.length-offHere,stopped:rows2.filter(r=>r.seen?.week).length,needs:seen?.needs??[]},tracks:[{id:"full",name:"Full",words:"On Full you answer the questions and approve the plan."},{id:"express",name:"Express",words:"On Express there are no questions and the plan is approved on its own. You still check the work."},{id:"auto",name:BUILDS_BY_ITSELF,words:"Approved in advance, it merges when the checks, the review and the proof pass. Release stays yours."}],stages,alwaysOn:always,limits:limitRows(meta).map(l=>seen?.hits[l.id]?{...l,hit:seen.hits[l.id]}:l),caps:Object.keys(HARNESS_CAP_NAME).map(key=>({key,name:HARNESS_CAP_NAME[key],...caps[key]})),agents:agentPowers(),additions:{items:file3.additions.map(a=>({...a,stage:additionStage(a),...seen?.rows[`add:${a.id}`]?{seen:seen.rows[`add:${a.id}`]}:{},...a.kind==="rule"&&ruleNote(a.text)?{note:ruleNote(a.text)}:{},...a.kind==="gate"&&a.part&&meta&&!partsOf(meta).some(p=>p.id===a.part)?{partGone:!0}:{}})),log:file3.log.slice(-200).reverse(),..."broken"in file3&&file3.broken?{broken:file3.broken}:{},parts:meta&&severalParts(meta)?partsOf(meta).filter(p=>!p.shared).map(p=>({id:p.id,name:p.name})):[],guarded:guardSupport().on,demo:meta?.engine!=="live",shared:bp?docsInGit(bp):!1,limits:{rules:MAX_RULES,ruleChars:MAX_RULE_CHARS,questions:MAX_CHECKLIST_QUESTIONS}},safety:{hidden:[...PRIVATE.map(p=>HARNESS_PRIVATE_NAME[p]??p),"Your AI tools' sign-ins","Browser sign-ins","Keys in package settings",".env files","squad-kit's own folder and keys"],locked:["squad-kit's own folder","AI instruction files","Commit hooks and their settings","Code history: commit and branches. A push is refused, and agents have no keys to push with"],sites:levelSites(level),network:{level,levels:NETWORK_LEVELS.map(id2=>({id:id2,name:NETWORK_NAME[id2],words:NETWORK_WORDS[id2]})),closed:KNOWN_BAD_GROUPS.map(g=>g.name),commands:networkPolicy(level).commands},refused:Object.values(HARNESS_REFUSED),tools:Object.values(HARNESS_TOOL_NAME),local:(meta?.localFiles??[]).map(f=>f.path),lockedByYou:(meta?.lockedFiles??[]).map(f=>f.path)}}}var ARABIC=/[؀-ۿݐ-ݿࢠ-ࣿﭐ-﷿ﹰ-]/g;function isArabic(text3){let ar=text3.match(ARABIC)?.length??0,la=text3.match(/[A-Za-z]/g)?.length??0;return ar>0&&ar>=la}function plainArabic(text3){return text3.replace(/[ً-ْٰـ]/g,"").replace(/[أإآٱ]/g,"\u0627").replace(/ة/g,"\u0647").replace(/ى/g,"\u064A").replace(/ؤ/g,"\u0648").replace(/ئ/g,"\u064A")}var EN_POLITE=/^(please\s+)?((can|could|would) you\s+)?(please\s+)?(tell me|let me know|show me|do you know|i want to know|i'd like to know)\s+/,EN_DO=/^(make|change|add|remove|delete|use|put|rename|move|show|hide|replace|set|turn|fix|update|switch|include|drop|give|let|don't|dont|do not|never|always|write|create)\b/,EN_ASK=/\b(i want|i'd like|i would like|we need|we want|it should|they should|can you|could you|would you|please)\b/,EN=[["cost",/\b(cost|costs|costing|spend|spent|spending|price|money|bill|budget|tokens?|dollars?)\b|\$|how much (has|did|does|is|will)\b/],["problem",/\b(problems?|issues?|wrong|stuck|broken|errors?|fail|failed|failing|ok|okay|fine|alright|all right|slow|hang|hanging|hung|blocked|trouble|not working|(so|too) long|taking (so )?long)\b/],["when",/\b(when|how long|eta|ready|finish|finished|done|time left|soon|how much longer|how many minutes|take)\b/],["next",/\b(next|after this|after that|then what|what comes|coming up)\b/],["doing",/\b(doing|working|status|progress|where is|where's|happening|happened|up to|right now|how('s| is) it going|what (did|has) (it|he|she|they))\b/]],AR_POLITE=/^(لو سمحت|من فضلك|رجاء)?\s*(ممكن\s+)?(تقول لي|تقولي|قل لي|قول لي|قلي|خبرني|اخبرني|ابي اعرف|ابغي اعرف|ابغا اعرف|اريد ان اعرف|اريد اعرف|ودي اعرف)\s+/,AR_DO=/^(خل|خلي|خليه|غير|اضف|ضيف|زود|احذف|شيل|امسح|حط|ضع|اجعل|بدل|عدل|سو|سوي|اكتب|ابي|ابغي|ابغا|اريد|نبي|نبغي|لازم|يجب|ممكن|استخدم|اعرض|اخف)(\s|$)/,AR=[["cost",/(تكلف|كلف|تكاليف|سعر|فلوس|مصاريف|مصروف|صرف|دفع|توكن|دولار)/],["when",/(متي|امتي|ايمتي|كم باقي|كم يحتاج|كم ياخذ|كم وقت|كم دقيقه|يخلص|تخلص|خلص|ينتهي|تنتهي|انتهي|جاهز|يجهز|تجهز)/],["problem",/(مشكل|عالق|معلق|واقف|متوقف|خطا|غلط|تمام|سليم|بطي|متاخر|طول|طولت|عطل|خربان|في شي|فيه شي)/],["next",/(التالي|التاليه|الجاي|الجايه|القادم|القادمه|بعدين|بعد كذا|بعد ذلك|بعدها|وش بعد|ايش بعد|شو بعد)/],["doing",/(يسوي|تسوي|يعمل|تعمل|يشتغل|تشتغل|يفعل|تفعل|الحاله|وين وصل|فين وصل|وصل لوين|وش يصير|ايش يصير|شو عم|الحين|حاليا)/]];function askTopic(text3){if(isArabic(text3)){let s2=plainArabic(text3).replace(/[؟?!.،,]/g," ").replace(/\s+/g," ").trim(),q5=s2.replace(AR_POLITE,"");return q5!==s2?{topic:AR.find(([,re])=>re.test(q5))?.[0]??"other",lang:"ar"}:AR_DO.test(q5)?{topic:"change",lang:"ar"}:{topic:AR.find(([,re])=>re.test(q5))?.[0]??"other",lang:"ar"}}let s=text3.toLowerCase().replace(/[’`]/g,"'").replace(/[?!.,;:]+/g," ").replace(/\s+/g," ").trim(),q4=s.replace(EN_POLITE,"");if(q4===s&&EN_DO.test(q4))return{topic:"change",lang:"en"};let found2=EN.find(([,re])=>re.test(q4))?.[0];return found2?{topic:found2,lang:"en"}:{topic:q4===s&&EN_ASK.test(q4)?"change":"other",lang:"en"}}var WHO_AR={analyst:"\u0627\u0644\u0645\u062D\u0644\u0644",architect:"\u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A",builder:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631",reviewer:"\u0627\u0644\u0645\u0631\u0627\u062C\u0639",tester:"\u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631",squad:"squad-kit"},YOUR_PART_AR={questions:"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0627\u0644\u0623\u0633\u0626\u0644\u0629",spec:"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A",plan:"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u062A\u0635\u0645\u064A\u0645",proof:"\u0645\u0631\u0627\u062C\u0639\u0629 \u0627\u0644\u0635\u0648\u0631 \u0648\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u064A\u0647\u0627",stuck:"\u0627\u062E\u062A\u064A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629",release:"\u0625\u0635\u062F\u0627\u0631 \u0627\u0644\u0646\u0633\u062E\u0629",ask:"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0627\u0644\u0633\u0624\u0627\u0644",yours:"\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0625\u0644\u0649 \u0627\u0644\u0641\u0631\u064A\u0642",chat:"\u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629",change:"\u0627\u0644\u0628\u062A \u0641\u064A \u062A\u062D\u062F\u064A\u062B \u0627\u0644\u062A\u0635\u0645\u064A\u0645",depends:"\u0627\u062E\u062A\u064A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0627\u0644\u0628\u0646\u0627\u0621"},NEED_AR={"answer its questions, or approve the spec":"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0623\u0633\u0626\u0644\u062A\u0647 \u0623\u0648 \u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A","approve the spec":"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A","approve the design":"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u062A\u0635\u0645\u064A\u0645","check the proof":"\u0645\u0631\u0627\u062C\u0639\u0629 \u0627\u0644\u0635\u0648\u0631 \u0648\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u064A\u0647\u0627"};function arMinutes(seconds2){let m=Math.max(1,Math.round(seconds2/60));return m===1?"\u062F\u0642\u064A\u0642\u0629 \u0648\u0627\u062D\u062F\u0629":m===2?"\u062F\u0642\u064A\u0642\u062A\u064A\u0646":m<=10?`${m} \u062F\u0642\u0627\u0626\u0642`:`${m} \u062F\u0642\u064A\u0642\u0629`}function arIn(seconds2){return Math.round(seconds2/60)<=1?"\u062E\u0644\u0627\u0644 \u062F\u0642\u064A\u0642\u0629 \u062A\u0642\u0631\u064A\u0628\u064B\u0627":`\u062E\u0644\u0627\u0644 ${arMinutes(seconds2)} \u062A\u0642\u0631\u064A\u0628\u064B\u0627`}function arRange(u){if(u.high<60)return"\u0623\u0642\u0644 \u0645\u0646 \u062F\u0642\u064A\u0642\u0629";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`\u062D\u0648\u0627\u0644\u064A ${arMinutes(low*60)}`:`\u0645\u0646 ${low} \u0625\u0644\u0649 ${high} ${high<=10?"\u062F\u0642\u0627\u0626\u0642":"\u062F\u0642\u064A\u0642\u0629"}`}var VERB_AR=[[/^editing /,"\u064A\u0639\u062F\u0651\u0644 "],[/^reading /,"\u064A\u0642\u0631\u0623 "],[/^writing /,"\u064A\u0643\u062A\u0628 "],[/^running /,"\u064A\u0634\u063A\u0651\u0644 "],[/^searching for /,"\u064A\u0628\u062D\u062B \u0639\u0646 "],[/^looking for /,"\u064A\u0628\u062D\u062B \u0639\u0646 "],[/^looking up /,"\u064A\u0628\u062D\u062B \u0641\u064A "],[/^listing /,"\u064A\u0633\u062A\u0639\u0631\u0636 "]];function doingAr(doing){for(let[re,ar]of VERB_AR)if(re.test(doing))return doing.replace(re,ar);return doing}var SENTENCE_AR={questions:"\u0627\u0644\u0645\u062D\u0644\u0644 \u064A\u0642\u0631\u0623 \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0644\u064A\u0639\u0631\u0641 \u0645\u0627 \u064A\u062D\u062A\u0627\u062C \u0623\u0646 \u064A\u0633\u0623\u0644\u0643 \u0639\u0646\u0647.",spec:"\u0627\u0644\u0645\u062D\u0644\u0644 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A: \u0627\u0644\u0642\u0635\u0629 \u0648\u0634\u0631\u0648\u0637 \u0627\u0644\u0625\u0646\u062C\u0627\u0632.",design:"\u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A \u064A\u062E\u062A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0627\u0644\u0628\u0646\u0627\u0621 \u0648\u064A\u0643\u062A\u0628 \u0627\u0644\u062A\u0635\u0645\u064A\u0645.",build:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0643\u062A\u0628 \u0627\u0644\u0643\u0648\u062F.",fix:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0635\u0644\u062D \u0645\u0627 \u0648\u062C\u062F\u062A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0623\u0648 \u0627\u0644\u0645\u0631\u0627\u062C\u0639.",checks:"squad-kit \u064A\u0634\u063A\u0651\u0644 \u0641\u062D\u0648\u0635\u0627\u062A \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0628\u0646\u0641\u0633\u0647\u060C \u0641\u0644\u0627 \u064A\u0642\u064A\u0651\u0645 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0645\u0644\u0647 \u0628\u0646\u0641\u0633\u0647.",review:"\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u060C \u0648\u0647\u0648 \u0648\u0643\u064A\u0644 \u062C\u062F\u064A\u062F\u060C \u064A\u0642\u0631\u0623 \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A.",proof:"\u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u064A\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0648\u064A\u0644\u062A\u0642\u0637 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632."};function screens2(t){return changesScreens(t)}var MERGES_AR="\u064A\u0642\u0628\u0644 \u0627\u0644\u0641\u0631\u064A\u0642 \u0627\u0644\u0639\u0645\u0644 \u0648\u064A\u062F\u0645\u062C\u0647 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627.";function thenAr(t,now2){let shots=screens2(t)?"\u060C \u062B\u0645 \u064A\u0644\u062A\u0642\u0637 \u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632":"",ready=t.track==="auto"?MERGES_AR:"\u0627\u0644\u0625\u062B\u0628\u0627\u062A \u062C\u0627\u0647\u0632 \u0644\u062A\u0631\u0627\u062C\u0639\u0647.",p=now2.part;switch(now2.step){case"questions":return"\u0633\u064A\u0633\u0623\u0644\u0643 \u0627\u0644\u0645\u062D\u0644\u0644 \u0639\u0645\u0627 \u064A\u062D\u062A\u0627\u062C \u0645\u0639\u0631\u0641\u062A\u0647\u060C \u0623\u0648 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0645\u0628\u0627\u0634\u0631\u0629.";case"spec":return"\u062A\u0642\u0631\u0623 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u0627\u060C \u062B\u0645 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A \u0627\u0644\u062A\u0635\u0645\u064A\u0645.";case"design":return t.track==="express"?"\u0627\u0644\u0645\u0633\u0627\u0631 \u0627\u0644\u0633\u0631\u064A\u0639: \u062A\u064F\u0639\u062A\u0645\u062F \u0627\u0644\u062E\u0637\u0629 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0648\u062A\u0644\u064A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0648\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u0629.":t.track==="auto"?"\u0645\u0633\u0627\u0631 \xABBuilds by itself\xBB: \u062A\u064F\u0639\u062A\u0645\u062F \u0627\u0644\u062E\u0637\u0629 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0648\u062A\u0644\u064A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0648\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u0629.":"\u062A\u0642\u0631\u0623 \u0627\u0644\u062A\u0635\u0645\u064A\u0645 \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631.";case"build":return p&&p.n<p.of?`\u064A\u0643\u0645\u0644 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0627\u0644\u062E\u0637\u0648\u0629 ${p.n+1} \u0645\u0646 ${p.of}\u060C \u062B\u0645 \u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0648\u064A\u0642\u0631\u0623 \u0645\u0631\u0627\u062C\u0639 \u062C\u062F\u064A\u062F \u0627\u0644\u0639\u0645\u0644${shots}.`:`\u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0648\u064A\u0642\u0631\u0623 \u0645\u0631\u0627\u062C\u0639 \u062C\u062F\u064A\u062F \u0627\u0644\u0639\u0645\u0644${shots}.`;case"fix":return`\u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0645\u0631\u0629 \u0623\u062E\u0631\u0649 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0642\u0631\u0623 \u0627\u0644\u0645\u0631\u0627\u062C\u0639 \u0627\u0644\u0639\u0645\u0644${shots}.`;case"checks":return`\u064A\u0642\u0631\u0623 \u0627\u0644\u0645\u0631\u0627\u062C\u0639\u060C \u0648\u0647\u0648 \u0648\u0643\u064A\u0644 \u062C\u062F\u064A\u062F\u060C \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A${shots}.`;case"review":return screens2(t)?"\u064A\u0644\u062A\u0642\u0637 \u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632.":ready;default:return ready}}function noteLeft(now2){return now2.usual?Math.max(60,middle(now2.usual)-(now2.seconds??0)):60}function noteWhenAr(t,now2){let none="\u0644\u0627 \u064A\u0639\u0645\u0644 \u0623\u064A \u0648\u0643\u064A\u0644 \u0627\u0644\u0622\u0646\u060C \u0641\u0644\u0646 \u064A\u0642\u0631\u0623\u0647\u0627 \u0623\u062D\u062F \u0628\u0639\u062F.";switch(now2.state){case"working":return now2.role==="squad"?`squad-kit \u064A\u0634\u063A\u0651\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0627\u0644\u0622\u0646. \u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0648\u0643\u064A\u0644 \u0627\u0644\u062A\u0627\u0644\u064A \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647\u060C ${arIn(noteLeft(now2))}.`:`\u064A\u0642\u0631\u0624\u0647\u0627 ${WHO_AR[now2.role??"builder"]} \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C ${arIn(noteLeft(now2))}.`;case"you":return`${none} \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F\u0645\u0627 \u064A\u062A\u0627\u0628\u0639 \u0627\u0644\u0639\u0645\u0644\u060C \u0628\u0639\u062F ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`;case"queued":return`${none} \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631: \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646 \u0648\u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647\u0627.`;case"stopping":return"\u0627\u0644\u0641\u0631\u064A\u0642 \u064A\u062A\u0648\u0642\u0641 \u0627\u0644\u0622\u0646\u060C \u0648\u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0639\u0646\u062F \u0645\u062A\u0627\u0628\u0639\u0629 \u0627\u0644\u0639\u0645\u0644.";case"ready":case"done":return"\u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644\u060C \u0641\u0644\u0627 \u064A\u0639\u0645\u0644 \u0639\u0644\u064A\u0647 \u0623\u064A \u0648\u0643\u064A\u0644 \u0628\u0639\u062F \u0627\u0644\u0622\u0646. \u062A\u0628\u0642\u0649 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629 \u0641\u064A \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0644\u0644\u0633\u062C\u0644.";default:return`${none} \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F \u0628\u062F\u0621 \u0627\u0644\u0639\u0645\u0644 \u0639\u0644\u0649 \u0627\u0644\u062A\u0630\u0643\u0631\u0629.`}}function canPropose(t){return t.stage==="release"||t.stage==="done"||!!t.specApproved||["implement","test","review"].includes(t.stage)}function agentOf(now2){return now2.role&&now2.role!=="squad"?now2.role:"analyst"}function slowNow(now2,book,t){return now2.slow?!0:!now2.step||!now2.role||now2.seconds===void 0?!1:now2.seconds>=slowAfter(usualOf(book,now2.role,now2.step,t.track))}function costLine(t,lang2){let total=totalOf(t.usage);return total.runs?total.costUsd===void 0?lang2==="ar"?`\u0644\u0645 \u062A\u0630\u0643\u0631 \u0623\u062F\u0627\u0629 \u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064A \u062A\u0643\u0644\u0641\u0629 \u0647\u0630\u0647 \u0627\u0644\u062A\u0634\u063A\u064A\u0644\u0627\u062A (${total.runs}).`:`The AI tool did not report a cost for these ${total.runs} runs.`:lang2==="ar"?`\u0627\u0644\u062A\u0643\u0644\u0641\u0629 \u062D\u062A\u0649 \u0627\u0644\u0622\u0646 ${total.costUsd<.005?"\u0623\u0642\u0644 \u0645\u0646 0.01 \u062F\u0648\u0644\u0627\u0631":`${total.costUsd.toFixed(2)} \u062F\u0648\u0644\u0627\u0631`} \u0628\u0623\u0633\u0639\u0627\u0631 API\u060C \u0641\u064A ${total.runs} ${total.runs<=10?"\u0645\u0631\u0627\u062A \u062A\u0634\u063A\u064A\u0644":"\u0645\u0631\u0629 \u062A\u0634\u063A\u064A\u0644"}.`:`So far it has cost ${costWords(total)} at API prices, over ${total.runs} ${total.runs===1?"run":"runs"}.`:lang2==="ar"?"\u0644\u0645 \u064A\u064F\u062D\u0633\u0628 \u0634\u064A\u0621 \u0628\u0639\u062F: \u0644\u0645 \u064A\u0639\u0645\u0644 \u0623\u064A \u0648\u0643\u064A\u0644 \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062A\u0630\u0643\u0631\u0629.":"Nothing is counted yet: no agent has run on this ticket."}function problemLines(t,now2,book,lang2){let ar=lang2==="ar",secs=now2.seconds;if(now2.state==="working"){let who4=ar?WHO_AR[now2.role??"builder"]:`The ${now2.who}`;if(now2.slow?.kind==="stuck"){let quiet=now2.slow.text;return ar?[`\u0642\u062F \u064A\u0643\u0648\u0646 \u0627\u0644\u0639\u0645\u0644 \u0639\u0627\u0644\u0642\u064B\u0627. ${quiet.startsWith("No progress")?"\u0644\u0627 \u062A\u0642\u062F\u0651\u0645 \u0645\u0646\u0630 \u0641\u062A\u0631\u0629.":""}`.trim(),"\u0627\u0644\u0628\u0637\u0627\u0642\u0629 \u0623\u0639\u0644\u0627\u0647 \u062A\u0639\u0631\u0636 \u0645\u0627 \u0641\u062D\u0635\u0647 squad-kit \u0648\u0637\u0631\u0642 \u0627\u0644\u0645\u062A\u0627\u0628\u0639\u0629."]:[`It may be stuck. ${quiet}`,"The card above says what squad-kit checked, with the ways out."]}if(slowNow(now2,book,t))return!now2.slow&&!ar?[`It is slower than usual, but still working. ${now2.role==="squad"?"squad-kit":who4} has been on this step for ${minutesWords(secs??0)}. It usually takes ${rangeEn(now2.usual)}.`,"You can give it more time, stop and try again, or stop it."]:ar?[`\u0627\u0644\u0639\u0645\u0644 \u0623\u0628\u0637\u0623 \u0645\u0646 \u0627\u0644\u0645\u0639\u062A\u0627\u062F\u060C \u0644\u0643\u0646\u0647 \u0645\u0633\u062A\u0645\u0631. ${who4} \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u0645\u0646\u0630 ${arMinutes(secs??0)}\u060C \u0648\u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(now2.usual)}.`,"\u064A\u0645\u0643\u0646\u0643 \u0645\u0646 \u0627\u0644\u0628\u0637\u0627\u0642\u0629 \u0623\u0639\u0644\u0627\u0647 \u0645\u0646\u062D\u0647 10 \u062F\u0642\u0627\u0626\u0642 \u0625\u0636\u0627\u0641\u064A\u0629\u060C \u0623\u0648 \u0625\u064A\u0642\u0627\u0641\u0647 \u0648\u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0646 \u062C\u062F\u064A\u062F\u060C \u0623\u0648 \u0625\u064A\u0642\u0627\u0641\u0647."]:[`It is slower than usual, but still working. ${now2.slow?.text??""}`.trim(),"You can give it 10 more minutes, stop and try again, or stop it from the card above."];let p=now2.part,u=now2.usual,checks=now2.role==="squad",first2=ar?checks?`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. squad-kit \u064A\u0634\u063A\u0651\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A${secs!==void 0?` \u0645\u0646\u0630 ${arMinutes(secs)}`:" \u0627\u0644\u0622\u0646"}. \u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(u)}.`:`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ${who4}${p?` \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${p.n} \u0645\u0646 ${p.of} \u0648`:" "}\u064A\u0639\u0645\u0644${secs!==void 0?` \u0645\u0646\u0630 ${arMinutes(secs)}`:" \u0627\u0644\u0622\u0646"}. \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(u)}.`:checks?`No problem. squad-kit is running the checks${secs!==void 0?`, for ${minutesWords(secs)} so far`:""}. They usually take ${rangeEn(u)}.`:secs===void 0?`No problem. ${who4} is working${p?` on step ${p.n} of ${p.of}`:""}. This step usually takes ${rangeEn(u)}.`:`No problem. ${who4}${p?` is on step ${p.n} of ${p.of} and has worked for`:" has worked on this step for"} ${minutesWords(secs)}. This step usually takes ${rangeEn(u)}.`,tries=(t.tries??0)>0?ar?"\u0641\u0634\u0644\u062A \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0645\u0631\u0629 \u0639\u0644\u0649 \u0627\u0644\u0637\u0631\u064A\u0642\u060C \u0648\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0635\u0644\u062D\u0647\u0627.":`The checks failed ${t.tries===1?"once":`${t.tries} times`} on the way; the Builder fixes what they found.`:"",then=ar?`\u0628\u0639\u062F\u0647\u0627: ${thenAr(t,now2)} \u0633\u062A\u0638\u0647\u0631 \u0641\u064A \xABWaiting for you\xBB \u0639\u0646\u062F\u0645\u0627 \u062A\u062D\u062A\u0627\u062C\u0643.`:`Next: ${lowerFirst3(now2.then??"")} I will put it in Waiting for you when it needs you.`;return[first2,tries,then].filter(Boolean)}return now2.state==="you"?t.waiting?.kind==="stuck"?ar?["\u0646\u0639\u0645: \u062A\u0648\u0642\u0641 \u0627\u0644\u0641\u0631\u064A\u0642 \u0648\u064A\u062D\u062A\u0627\u062C \u0642\u0631\u0627\u0631\u0643.",t.stuck?.question??""].filter(Boolean):["Yes: the squad stopped and needs your decision.",t.stuck?.question??""].filter(Boolean):ar?[`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u062A\u0646\u062A\u0638\u0631 \u0645\u0646\u0643 ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[`No problem. ${now2.sentence}`]:ar?[stateAr(t,now2,"\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ")]:[`No problem. ${now2.sentence}`]}function stateAr(t,now2,lead=""){switch(now2.state){case"queued":return`${lead}\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631\u060C \u0648\u062A\u0628\u062F\u0623 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646.`;case"stopping":return"\u0627\u0644\u0639\u0645\u0644 \u064A\u062A\u0648\u0642\u0641 \u0643\u0645\u0627 \u0637\u0644\u0628\u062A. \u0643\u0644 \u0645\u0627 \u0623\u064F\u0646\u062C\u0632 \u0645\u062D\u0641\u0648\u0638.";case"ready":return`${lead}\u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644 \u0648\u064A\u0635\u062F\u0631 \u0645\u0639 ${t.release??"\u0627\u0644\u0646\u0633\u062E\u0629 \u0627\u0644\u0642\u0627\u062F\u0645\u0629"}.`;case"done":return`${lead}\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`;case"you":return`${lead}\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u062A\u0646\u062A\u0638\u0631 \u0645\u0646\u0643 ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`;default:return`${lead}\u0644\u0627 \u064A\u062C\u0631\u064A \u0623\u064A \u0639\u0645\u0644 \u0639\u0644\u064A\u0647\u0627 \u0627\u0644\u0622\u0646.`}}function rangeEn(u){if(u.high<60)return"under a minute";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`about ${low} minute${low===1?"":"s"}`:`${low} to ${high} minutes`}function lowerFirst3(s){return/^[A-Z][A-Z0-9]+\b/.test(s)?s:s.charAt(0).toLowerCase()+s.slice(1)}function whenLines(t,now2,book,lang2){let ar=lang2==="ar";if(now2.state==="working"&&now2.readyIn){if(slowNow(now2,book,t)){let who4=ar?WHO_AR[now2.role??"builder"]:now2.role==="squad"?"squad-kit":`The ${now2.who}`;return ar?[`\u0627\u0644\u0639\u0645\u0644 \u0623\u0628\u0637\u0623 \u0645\u0646 \u0627\u0644\u0645\u0639\u062A\u0627\u062F\u060C \u0644\u0630\u0644\u0643 \u064A\u0635\u0639\u0628 \u0627\u0644\u062A\u0642\u062F\u064A\u0631. ${who4} \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u0645\u0646\u0630 ${arMinutes(now2.seconds??0)}.`]:[`It is taking longer than usual, so it is hard to say. ${now2.slow?.text??`${who4} has been on this step for ${minutesWords(now2.seconds??0)}.`}`]}let what=now2.readyIn.what;return t.track==="auto"?ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 ${MERGES_AR}`]:[`Probably ${inAbout(now2.readyIn.seconds)}. Then it is accepted and merged by itself.`]:ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[what]??what}.`]:[`Probably ${inAbout(now2.readyIn.seconds)}. Then you will be asked to ${what}.`]}return now2.state==="you"?ar?[`\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0628\u0627\u0646\u062A\u0638\u0627\u0631\u0643 \u0627\u0644\u0622\u0646: \u062A\u062D\u062A\u0627\u062C ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[`It is waiting for you now: ${lowerFirst3(t.waiting?.summary??"give it back when you are done")}.`]:now2.state==="queued"?ar?["\u0644\u0645 \u062A\u0628\u062F\u0623 \u062E\u0637\u0648\u062A\u0647\u0627 \u0628\u0639\u062F: \u0647\u064A \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631\u060C \u0648\u062A\u0628\u062F\u0623 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646."]:["It is next in line, so its step has not started yet. It starts when a slot is free."]:now2.state==="ready"?ar?[`\u062C\u0627\u0647\u0632\u0629: \u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644 \u0648\u064A\u0635\u062F\u0631 \u0645\u0639 ${t.release??"\u0627\u0644\u0646\u0633\u062E\u0629 \u0627\u0644\u0642\u0627\u062F\u0645\u0629"}.`]:[`It is ready: accepted, and it goes out with ${t.release??"the next release"}.`]:now2.state==="done"?ar?[`\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`]:[now2.sentence]:now2.state==="idle"?ar?["\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0639\u0645\u0644 \u0639\u0644\u064A\u0647\u0627 \u0628\u0639\u062F."]:["Work on it has not started yet."]:ar?[stateAr(t,now2)]:[now2.sentence]}function doingLines(t,now2,lang2){if(lang2==="en")return[now2.sentence,...now2.state==="working"&&now2.doing?[`Right now it is ${now2.doing}.`]:[]];if(now2.state!=="working")return[stateAr(t,now2)];let who4=WHO_AR[now2.role??"builder"],title2=now2.part?t.steps?.[now2.part.n-1]?.title:void 0;return[now2.part&&title2?`${who4} \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${now2.part.n} \u0645\u0646 ${now2.part.of}: ${title2}.`:SENTENCE_AR[now2.step??"build"],...now2.doing?[`\u0627\u0644\u0622\u0646 ${doingAr(now2.doing)}.`]:[]]}function nextLines(t,now2,lang2){let ar=lang2==="ar";return now2.state==="working"&&now2.readyIn?ar?t.track==="auto"?[thenAr(t,now2),`\u064A\u064F\u0642\u0628\u0644 \u0627\u0644\u0639\u0645\u0644 \u0648\u064A\u064F\u062F\u0645\u062C \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[thenAr(t,now2),`\u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[now2.readyIn.what]??now2.readyIn.what} ${arIn(now2.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[now2.next??""]:now2.state==="you"?ar?[`\u064A\u062A\u0627\u0628\u0639 \u0627\u0644\u0641\u0631\u064A\u0642 \u0628\u0639\u062F ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[now2.next??now2.sentence]:ar?[stateAr(t,now2)]:[now2.next??now2.sentence]}function answerAsk(t,now2,book,question){let{topic,lang:lang2}=askTopic(question),ar=lang2==="ar",who4=agentOf(now2),to=now2.state==="working"&&now2.role&&now2.role!=="squad"?now2.role:void 0,note2={do:"note",label:ar?`\u0623\u0631\u0633\u0644\u0647\u0627 \u0645\u0644\u0627\u062D\u0638\u0629 \u0625\u0644\u0649 ${to?WHO_AR[to]:"\u0627\u0644\u0641\u0631\u064A\u0642"}`:`Send it as a note to the ${to?WHO[to]:"squad"}`,note:ar?noteWhenAr(t,now2):now2.noteWhen,...to?{to}:{}},chat={do:"chat",label:ar?`\u0627\u0633\u0623\u0644 ${WHO_AR[who4]}`:`Ask the ${WHO[who4]}`,to:who4},stop={do:"stop",label:ar?"\u0623\u0648\u0642\u0641 \u0627\u0644\u0639\u0645\u0644":"Stop the work"},answer2=(lines3,offers=[],instant=!0)=>({lang:lang2,topic,instant,lines:lines3.filter(Boolean),offers});switch(topic){case"problem":return answer2(problemLines(t,now2,book,lang2),now2.state==="working"&&!now2.slow?[stop]:[]);case"when":return answer2(whenLines(t,now2,book,lang2));case"doing":return answer2(doingLines(t,now2,lang2));case"next":return answer2(nextLines(t,now2,lang2));case"cost":return answer2([costLine(t,lang2)]);case"change":{if(t.stage==="release"||t.stage==="done"){let line3=ar?`${t.id} \u0645\u0642\u0628\u0648\u0644\u0629\u060C \u0644\u0630\u0644\u0643 \u064A\u0635\u0628\u062D \u0623\u064A \u062A\u063A\u064A\u064A\u0631 \u062A\u0630\u0643\u0631\u0629 \u062C\u062F\u064A\u062F\u0629 \u0645\u0631\u062A\u0628\u0637\u0629 \u0628\u0647\u0627 \u0623\u0648 \u0628\u0644\u0627\u063A \u062E\u0637\u0623.`:`${t.id} is accepted, so a change becomes a new linked ticket or a bug report.`;return answer2([line3],[{do:"propose",label:ar?"\u0627\u0637\u0644\u0628 \u062A\u063A\u064A\u064A\u0631\u064B\u0627":"Ask for a change",note:ar?"\u062A\u062E\u062A\u0627\u0631: \u062A\u0630\u0643\u0631\u0629 \u062C\u062F\u064A\u062F\u0629 \u0645\u0631\u062A\u0628\u0637\u0629 \u0623\u0648 \u0628\u0644\u0627\u063A \u062E\u0637\u0623.":"You choose: a new linked ticket or a bug report."}],!1)}return canPropose(t)?answer2([ar?"\u0647\u0630\u0627 \u064A\u063A\u064A\u0651\u0631 \u0645\u0627 \u0648\u0627\u0641\u0642\u062A \u0639\u0644\u064A\u0647 \u0641\u064A \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A. \u0643\u064A\u0641 \u062A\u0631\u064A\u062F \u0623\u0646 \u064A\u0635\u0644 \u0625\u0644\u0649 \u0627\u0644\u0641\u0631\u064A\u0642\u061F":"That changes what you approved in the spec. How should it reach the squad?"],[{do:"propose",label:ar?"\u0627\u0642\u062A\u0631\u062D \u062A\u0639\u062F\u064A\u0644\u064B\u0627":"Propose a change",note:ar?"\u062A\u064F\u062D\u062F\u064E\u0651\u062B \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u0627\u060C \u062B\u0645 \u064A\u0639\u064A\u062F \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0645\u0627 \u062A\u063A\u064A\u0651\u0631 \u0641\u0642\u0637.":"The spec is updated, you approve it, and the Builder redoes only what changed."},note2],!1):answer2([ar?"\u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0644\u0645 \u062A\u064F\u0639\u062A\u0645\u062F \u0628\u0639\u062F\u060C \u0641\u0645\u0627 \u0632\u0627\u0644 \u0627\u0644\u0641\u0631\u064A\u0642 \u064A\u0633\u062A\u0637\u064A\u0639 \u0623\u062E\u0630 \u0630\u0644\u0643 \u0641\u064A \u0627\u0644\u062D\u0633\u0628\u0627\u0646. \u0623\u0631\u0633\u0644\u0647\u0627 \u0645\u0644\u0627\u062D\u0638\u0629\u060C \u0623\u0648 \u0646\u0627\u0642\u0634\u0647\u0627 \u0645\u0639 \u0627\u0644\u0645\u062D\u0644\u0644.":"The spec is not approved yet, so the squad can still take this in. Send it as a note, or talk it through with the Analyst."],[note2,{do:"chat",label:ar?"\u0646\u0627\u0642\u0634\u0647\u0627 \u0645\u0639 \u0627\u0644\u0645\u062D\u0644\u0644":"Talk it through with the Analyst",to:"analyst"}],!1)}default:return answer2([ar?"\u0623\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0641\u0648\u0631\u064B\u0627 \u0639\u0646 \u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u062D\u0627\u0644\u0629: \u0647\u0644 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629\u060C \u0645\u062A\u0649 \u064A\u062C\u0647\u0632\u060C \u0645\u0627\u0630\u0627 \u064A\u0639\u0645\u0644 \u0627\u0644\u0622\u0646\u060C \u0645\u0627 \u0627\u0644\u062E\u0637\u0648\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C \u0648\u0643\u0645 \u0643\u0644\u0651\u0641. \u0644\u0623\u064A \u0633\u0624\u0627\u0644 \u0622\u062E\u0631 \u0627\u0633\u0623\u0644 \u0623\u062D\u062F \u0627\u0644\u0648\u0643\u0644\u0627\u0621.":"I answer status questions at once: is there a problem, when will it be ready, what is it doing, what is next, and what it has cost. For anything else, ask an agent."],[chat],!1)}}import path137 from"path";var STEP_NAME={questions:"Questions",spec:"Spec",design:"Design",build:"Build",fix:"Fixes",checks:"Checks",review:"Review",proof:"Proof"},SESSION={questions:"intake",spec:"intake",design:"intake",build:"build",fix:"build",checks:void 0,review:"review",proof:"tester"},ENDS=[{step:"questions",who:"analyst",by:["analyst"],re:/^Asked \d+ questions?$/},{step:"spec",who:"analyst",by:["analyst"],re:/^(Wrote|Updated) the spec\b/},{step:"design",who:"architect",by:["architect","spec"],re:/^(Wrote|Updated) the (design|plan)\b/},{step:"build",who:"builder",by:["builder"],re:/^Finished all steps$/},{step:"fix",who:"builder",by:["builder"],re:/^Fixed (the|what)\b/},{step:"checks",who:"squad",by:["tester"],re:/^(All (\d+ )?checks pass|Checks failed|Checks still fail|No test, lint or build commands)/},{step:"review",who:"reviewer",by:["reviewer"],re:/^(Approved\b|Asked for \d+ fix|Still asks for)/},{step:"proof",who:"tester",by:["tester"],re:/^(Saved \d+ screenshots?|No screenshots were taken|No screens to show|No screens change)/}],AFTER={questions:[{re:/^Answered \d+ questions?/,who:["you"],note:"you answered"}],spec:[{re:/^Approved the spec/,who:["you"],note:"you approved"}],design:[{re:/^Approved the (design|plan)/,who:["you"],note:"you approved"},{re:/^Express track: approved the plan on its own/,who:["squad"],note:"approved on its own (Express track)"},{re:/^Builds by itself: approved the plan on its own/,who:["squad"],note:"approved on its own (Builds by itself)"}],proof:[{re:/^Checked the proof\. Looks right/,who:["you"],note:"you checked it: Looks right"},{re:/^(.+ passed: a|A)ccepted by itself/,who:["squad"],note:"accepted by itself"}]},TOOLISH=/^(Reading|Running|Editing|Writing|Looking|Searching|Listing|Runs on|Asked|Started|Finished|squad-kit|The answer was not complete)\b/,REFUSED=/User refused permission to run tool|^Permission denied: |haven't granted it yet/i,ms=iso2=>iso2?Date.parse(iso2):NaN;function oldestFirst(t){return[...t.activity].sort((a,b)=>a.at.localeCompare(b.at))}function runsOf2(t,log3){let history=t.history??[],first2=history.length?ms(history[0].from):1/0,fromLog=[];for(let a of log3){if(ms(a.at)>=first2)break;let end=ENDS.find(e=>e.by.includes(a.who)&&e.re.test(a.text));end&&fromLog.push({who:end.who,step:end.step,to:a.at,line:a})}let timed=history.map(h=>({...h,line:endLine(log3,h)}));return[...fromLog,...timed]}function endLine(log3,h){let end=ENDS.find(e=>e.step===h.step),lo=ms(h.from)-1e3,hi=ms(h.to)+6e4;return log3.find(a=>ms(a.at)>=lo&&ms(a.at)<=hi&&end.by.includes(a.who)&&end.re.test(a.text))}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run6){let end=ms(run6.line?.at??run6.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run6.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf(t,run6,log3){let line3=run6.line?.text??"";switch(run6.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read6=analystSummary(log3,run6),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read6?`${read6.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q4=>q4.answer).length,points=t.criteria?.length??0;return`Wrote the spec${/from your talk/.test(line3)?" from your talk":answers2?` from your ${plural4(answers2,"answer")}`:""}${points?`, with ${plural4(points,'"done when" point')}`:""}.`}case"design":{if(/^Updated the (design|plan): /.test(line3))return line3.replace(/\.?$/,".");let steps=t.steps?.length??0,said=t.decision?.options.find(o=>o.chosen)?.name?.replace(/^(A|An|The|One|Its|Their|Our|Your)\b/,w=>w.toLowerCase()),parts2=[said?`Chose ${said}`:"Wrote the design",steps?`planned ${plural4(steps,"step")} to build`:"",t.mockup?"made a mockup":""].filter(Boolean);return`${parts2.length>1?`${parts2.slice(0,-1).join(", ")} and ${parts2.at(-1)}`:parts2[0]}.`}case"build":{let steps=t.steps?.length??0;return steps?`Did the ${plural4(steps,"step")} of the design.`:"Built the change."}case"fix":return line3?line3.replace(/\.?$/,"."):"Fixed what the checks or the review found.";case"checks":return/^All/.test(line3)?`squad-kit ran the project's checks: ${line3.replace(/^All /,"all ").replace(/\.?$/,".")}`:line3?`squad-kit ran the project's checks. ${line3.replace(/\.?$/,".")}`:"squad-kit ran the project's checks.";case"review":return/^Approved: /.test(line3)?line3.replace(/\.?$/,"."):/^Approved/.test(line3)?"Approved the changes.":line3?`${line3.split(":")[0]}.`:"Read the changes.";case"proof":{let n=t.proof?.length??0,pictures=(t.proof??[]).reduce((sum,p)=>sum+1+(p.more?.length??0),0);return pictures>n?`Opened the app and took ${plural4(pictures,"screenshot")} of the ${plural4(n,'"done when" point')} it could show.`:n?`Opened the app and took ${plural4(n,"screenshot")}, one for each "done when" point it could show.`:line3?line3.replace(/\.?$/,"."):"Checked the change."}}}function filesFrom(conv,cwd,from,to){let added2=new Set,changed=new Set;if(!conv)return{added:[],changed:[]};let lo=ms(from),hi=ms(to),rel=p=>cwd&&p.startsWith(`${cwd}/`)?p.slice(cwd.length+1):path137.basename(p);for(let e of conv.updates){let at3=ms(e.at);if(Number.isFinite(lo)&&Number.isFinite(at3)&&(at3<lo||at3>hi))continue;let u=e.update;if(!(u.sessionUpdate!=="tool_call"&&u.sessionUpdate!=="tool_call_update"))for(let c of u.content??[]){if(c.type!=="diff"||!c.path)continue;let p=rel(c.path);c.oldText==null&&!changed.has(p)?added2.add(p):added2.has(p)||changed.add(p)}}return{added:[...added2],changed:[...changed]}}function handedIn(t,run6,files,conv){switch(run6.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q4=>({from:q4.from,text:q4.text,...q4.answer?{answer:q4.answer.value?`${q4.answer.choice.replace(/:$/,"")}: ${q4.answer.value}`:q4.answer.choice}:{}}))}:void 0;case"spec":return t.story||t.criteria?.length?{kind:"spec",...t.story?{story:t.story}:{},criteria:(t.criteria??[]).map(c=>c.then)}:void 0;case"design":{if(!t.decision&&!t.steps?.length)return;let chosen2=t.decision?.options.find(o=>o.chosen)?.name;return{kind:"design",...t.decision?{title:t.decision.title}:{},...chosen2?{chosen:chosen2}:{},...t.decision?.adr?{adr:t.decision.adr}:{},steps:(t.steps??[]).map(s=>s.title),mockup:!!t.mockup,follows:t.follows??[]}}case"build":case"fix":{let mine2=run6.step==="fix"||!files?filesFrom(conv,conv?.cwd,run6.from,run6.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run6.step==="build"?(t.steps??[]).filter(s=>s.status==="done").map(s=>s.title):[];return!got.added.length&&!got.changed.length&&!got.removed.length&&!steps.length?void 0:{kind:"files",added:got.added,changed:got.changed,removed:got.removed,steps}}case"checks":return t.gates?.checks&&t.gates.checks!=="pending"?{kind:"checks",ok:t.gates.checks==="pass"||t.gates.checks==="skipped",text:t.gates.checksText??(t.gates.checks==="pass"?"All checks pass":"Some checks failed")}:void 0;case"review":{let line3=run6.line?.text??"",approved=/^Approved/.test(line3)||!line3&&t.gates?.review==="pass",notes=approved?[]:t.reviewNotes?.length?t.reviewNotes:line3.includes(": ")?[line3.slice(line3.indexOf(": ")+2)]:[];return{kind:"verdict",approved,notes}}case"proof":return t.proof?.length?{kind:"shots",shots:t.proof.flatMap(p=>[p.after,...(p.more??[]).map(m=>m.after)].filter(file3=>!file3.endsWith(".api.json")).map(file3=>({criterion:p.criterion,file:file3})))}:void 0}}var TAB={spec:"plan",design:"plan",build:"changes",fix:"changes",checks:"proof",proof:"proof"};function counts2(conv,from,to){if(!conv||!from)return null;let lo=ms(from),hi=to?ms(to):1/0,inside5=at3=>{let x=ms(at3);return Number.isFinite(x)&&x>=lo-1e3&&x<=hi+1e3},actions=0,skipped=0;for(let e of conv.updates){if(!inside5(e.at))continue;let u=e.update;u.sessionUpdate==="tool_call"&&(actions+=1),u.sessionUpdate==="tool_call_update"&&u.status==="failed"&&(u.content??[]).some(c=>c.type==="content"&&c.content?.type==="text"&&REFUSED.test(c.content.text??""))&&(skipped+=1)}return{actions,skipped}}function whatHappened(bp,t,src={}){let now2=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read6=src.conversation??(id2=>readConversation(id2,3e3,void 0,bp.chatsDir)),convs=new Map,conv=key=>{let id2=key?t.sessions?.[key]:void 0;if(!id2)return null;if(!convs.has(id2))try{convs.set(id2,read6(id2))}catch{convs.set(id2,null)}return convs.get(id2)??null},files,ticketFiles=()=>{if(files===void 0){try{files=src.files?.()??null}catch{files=null}files&&!files.added.length&&!files.changed.length&&!files.removed.length&&(files=null)}return files},latest=new Map;runs2.forEach((r,i)=>latest.set(r.step,i));let reviews=runs2.filter(r=>r.step==="review").length,steps=runs2.map((run6,i)=>{let next=runs2[i+1],session=SESSION[run6.step],c=conv(session),isLatest=latest.get(run6.step)===i,seconds2=run6.from?Math.max(0,(ms(run6.to)-ms(run6.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run6.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run6.to)-1e3&&ms(l.at)<=until)&&notes.push(a.note);run6.step==="review"&&notes.push(reviews>1?`round ${runs2.slice(0,i+1).filter(r=>r.step==="review").length} of ${reviews}, a fresh session`:"a second, separate session");let got=isLatest?handedIn(t,run6,run6.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run6.from,run6.to),tab=TAB[run6.step];return{who:run6.who,step:run6.step,name:`${WHO[run6.who]} \xB7 ${STEP_NAME[run6.step]}`,...run6.from?{from:run6.from}:{},to:run6.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run6.step==="build"&&changed?summaryOf(t,run6,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf(t,run6,log3),...got?{handedIn:got}:{},...got&&tab?{tab}:{},notes,...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}}}),ck=t.clock;if(ck&&!t.waiting){let from=ck.began??ck.since,session=SESSION[ck.step],n=counts2(conv(session),from),part=ck.step==="build"&&ck.part&&t.steps?.length?` Step ${ck.part} of ${t.steps.length}.`:"";steps.push({who:ck.who,step:ck.step,name:`${WHO[ck.who]} \xB7 ${STEP_NAME[ck.step]}`,from,seconds:Math.max(0,(now2-ms(from))/1e3),running:!0,summary:`${(ck.doing??t.status)||"Working on it"}${ck.doing?".":""}${part}`.replace(/\.\./g,"."),notes:[],...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}})}return{steps,seconds:steps.reduce((s,x)=>s+(x.seconds??0),0)}}async function body2(c){try{return await c.req.json()}catch{return{}}}async function changedFiles3(bp,id2){let{files}=await ticketChanges(bp,id2);return files.length?{added:files.filter(f=>f.status==="added").map(f=>f.path),changed:files.filter(f=>f.status==="changed"||f.status==="renamed").map(f=>f.path),removed:files.filter(f=>f.status==="deleted").map(f=>f.path)}:null}function mountCostApi(app,bp){app.get("/api/v1/tickets/:id/happened",async c=>{let t=readTicket(bp,c.req.param("id"));if(!t)return c.json({error:"not found"},404);let files=await changedFiles3(bp,t.id).catch(()=>null);return c.json(whatHappened(bp,t,{files:()=>files}))}),app.get("/api/v1/tickets/:id/cost",async c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketCost(bp,t,await howPaid(bp))):c.json({error:"not found"},404)}),app.get("/api/v1/cost",async c=>boardExists(bp)?c.json(costOverview(bp,await howPaid(bp))):c.json({error:"No board here yet."},404)),app.put("/api/v1/cost/payment",async c=>{let b=await body2(c);return b.payment!=="subscription"&&b.payment!=="api-key"&&b.payment!=="auto"?c.json({error:"Pick subscription, api-key or auto."},400):(setPaymentChoice(b.payment),c.json(costOverview(bp,await howPaid(bp))))}),app.put("/api/v1/cost/budget",async c=>{let b=await body2(c);try{let change={};"ticketUsd"in b&&(change.ticketUsd=parseUsd(b.ticketUsd)),"monthUsd"in b&&(change.monthUsd=parseUsd(b.monthUsd));let budget=setBudget(bp,change);return c.json({...costOverview(bp,await howPaid(bp)),budget})}catch(err2){return c.json({error:plainWords(err2,"Saving the budget")},400)}})}import{createHash as createHash12}from"crypto";import fs145 from"fs";import path138 from"path";function docsHash(bp,id2){let said=["spec.md","design.md","questions.md","proof.md"].flatMap(f=>{try{return[f,fs145.readFileSync(path138.join(ticketDir(bp,id2),f),"utf8")]}catch{return[]}});return said.length?createHash12("sha256").update(said.join("\0")).digest("hex").slice(0,16):NO_DOCS}var MOST_HASHED=20*1024*1024;async function workOf2(meta,t){let hash2=createHash12("sha256"),built=!1;for(let c of copiesOf(meta,t)){if(!c.baseCommit||!fs145.existsSync(c.dir))continue;let files=(await workSoFar(c.dir,c.baseCommit)).sort();if(files.length){built=!0,hash2.update(await gitLater(c.dir,["rev-parse","HEAD"],{allowFail:!0}));for(let file3 of files){hash2.update(`\0${file3}\0`);try{let full=path138.join(c.dir,file3),seen=await fs145.promises.stat(full);hash2.update(seen.size>MOST_HASHED?`${seen.size}:${seen.mtimeMs}`:await fs145.promises.readFile(full))}catch{}}}}return{built,hash:built?hash2.digest("hex").slice(0,16):"none"}}function refuseLeaving(bp,t,meta){if(t.takenOver)throw new BoardActionError(`${t.id} is yours already. Give it back first.`);if(!isMine(t,meta))throw new BoardActionError(`${t.id} is ${ownerOf(t)}'s. Only they can move it.`);if(t.deleting)throw new BoardActionError(`${t.id} is being deleted.`);if(t.archived)throw new BoardActionError(`${t.id} is archived.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already. Report a bug or add a new ticket instead.`);if(t.chat?.open&&!t.chat.beside)throw new BoardActionError(`${t.id} has a chat open. Finish the talk first.`);let change=openChange(t);if(change)throw new BoardActionError(`${t.id} has a change still open (${change.id}). Settle it first.`);if(squadAlive(bp)&&(t.live||t.squad||t.stopRequested||t.questions?.some(q4=>q4.thinking)))throw new BoardActionError(`${t.id} is working right now. Wait until it needs you, or press Stop first.`)}async function workOutside(bp,id2){refuseWhileSaving(bp,id2);let meta=readMeta(bp);if(meta.engine!=="live")throw new BoardActionError("The sample board has no real project to work on outside.");let before=requireTicket(bp,id2);if(before.takenOver?.free)return before;refuseLeaving(bp,before,meta);let work=await workOf2(meta,before),docs=docsHash(bp,id2);return updateTicket(bp,id2,t=>{t.takenOver?.free||(refuseLeaving(bp,t,meta),leaveForOutside(t,meta.user.name,new Date().toISOString(),{docs,work:work.hash}),addActivity(t,"you","Moved it to a conversation outside squad-kit. The squad waits"))})}function asItWas(t,left){t.stage=left.stage,t.status=left.status,t.waiting=left.waiting,t.live=!1,left.phase&&!left.waiting&&(t.live=!0,scheduleSquad(t,left.phase,.2,1)),addActivity(t,"you","Brought it back. Nothing changed outside")}var BUILT_OUTSIDE="Built outside squad-kit, and squad-kit has not checked it";async function bringBack2(bp,id2){let meta=readMeta(bp),before=requireTicket(bp,id2);if(!before.takenOver?.free)throw new BoardActionError(`${before.id} is not in a conversation outside squad-kit.`);let work=await workOf2(meta,before),docs=docsHash(bp,id2),approvedBy=`${meta.user.name} (outside squad-kit)`,approveDesign=!1,back=updateTicket(bp,id2,t=>{let taken=t.takenOver,left=taken?.free;if(!taken||!left)throw new BoardActionError(`${t.id} is back already.`);if(t.takenOver=void 0,t.stopRequested=void 0,t.retryOnStop=void 0,t.waiting=null,t.squad=null,docs===left.docs&&work.hash===left.work&&!left.approved)return asItWas(t,left);t.outside=t.outside?{...t.outside,checked:void 0}:{...left.from?{from:left.from}:{},by:taken.by,at:taken.at},t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.offScreen=void 0,t.proofRound=void 0,t.live=!1,work.built?(left.designApproved&&(t.decision=t.decision&&{...t.decision,adr:t.decision.adr??saveDesignDecision(bp,t,approvedBy)}),t.steps=t.steps?.map(s=>({...s,status:"done"})),t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="review",t.status="Back from outside. squad-kit has not checked it",t.waiting={kind:"proof",summary:BUILT_OUTSIDE},addActivity(t,"you","Brought it back with code built outside squad-kit")):hasDesign(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a design written outside squad-kit"),left.designApproved?(approveDesign=!0,t.status="Design approved in the conversation",t.waiting={kind:"plan",summary:"The design was approved in the conversation"}):t.track==="express"||t.track==="auto"?(t.live=!0,t.status="Approving the plan",scheduleSquad(t,"express-approve",.2,1)):(t.status="Design ready for you",t.waiting={kind:"plan",summary:"The design was written outside squad-kit: is this how to build it?"})):hasSpec(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a spec written outside squad-kit"),needsSpec(t)?(t.status="Spec ready for you",t.waiting={kind:"spec",summary:"The spec was written outside squad-kit: is this what you want?"}):(t.live=!0,t.status="Architect is writing the design",scheduleSquad(t,"plan-write",.2,1))):(t.stage="tickets",t.live=!0,t.status="Analyst is reading the ticket",addActivity(t,"you","Brought it back. The squad carries on from the start"),scheduleSquad(t,"analyst-read",.2,1))});return approveDesign?approve(bp,back.id):back}function recordApproval(bp,id2,what){let meta=readMeta(bp);return updateTicket(bp,id2,t=>{let left=t.takenOver?.free;if(!left)throw new BoardActionError(`${t.id} is not in a conversation outside squad-kit.`);if(what==="spec"?!hasSpec(t):!hasDesign(t))throw new BoardActionError(`${t.id} has no ${what} written yet, so there is nothing to approve.`);t.specApproved??={by:`${meta.user.name} (outside squad-kit)`,at:new Date().toISOString()},left.approved=!0,what==="design"&&(left.designApproved=!0),addActivity(t,"you",`Approved the ${what} in the conversation outside squad-kit`)})}function checkOutside(bp,id2){return refuseWhileSaving(bp,id2),updateTicket(bp,id2,t=>{if(!t.outside)throw new BoardActionError(`${t.id} was not worked on outside squad-kit.`);if(t.takenOver)throw new BoardActionError(`${t.id} is still in the conversation. Bring it back first.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already.`);if(!waitsAtReview(t))throw new BoardActionError(`${t.id} has no built work waiting to be checked right now.`);if(!t.criteria?.length)throw new BoardActionError(`${t.id} has no "Done when" list in its spec.md, so there is nothing to check it against. Add one first.`);t.outside={...t.outside,checked:void 0},t.waiting=null,t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.proofRound=void 0,t.live=!0,t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="test",t.status="Running the project checks on the work done outside",addActivity(t,"you","Asked squad-kit to check the work done outside"),scheduleSquad(t,"test",.2,1)})}import{execFileSync as execFileSync16}from"child_process";import fs146 from"fs";import path139 from"path";var GUIDE_FILE="how-to-work-here.md",SAMPLE_DOCS=ticketDocs({id:"SK-1",title:"Sign in with a phone number",story:"People with no email can sign in with their phone number.",criteria:[{given:"someone on the sign-in page",when:"they enter their phone number and the code sent to it",then:"they are signed in"},{given:"someone on the sign-in page",when:"they enter a wrong code three times",then:"they wait ten minutes before trying again"}],releaseNote:"You can now sign in with your phone number.",questions:[{id:"q1",from:"analyst",text:"Should a code expire?",options:["After 5 minutes","After 30 minutes"],answer:{choice:"After 5 minutes"}}],decision:{title:"Where the codes are kept",area:"Data",options:[{name:"In the database",note:"Survives a restart",chosen:!0},{name:"In memory",note:"Simple, but lost on a restart"}],why:"A code must still work after a restart."},steps:[{title:"Add the codes table",status:"todo"},{title:"Send a code and check it",status:"todo"},{title:"Add the phone number to the sign-in page",status:"todo"}]});function shape(file3){return`\`\`\`\`markdown
1838
+ `}return at3.body.trim()&&out18.push(at3),out18}var boxesOf=body10=>[...body10.matchAll(/^\s*[-*]\s+\[[ xX]\]\s+(.+)$/gm)].map(m=>m[1].trim()),namedFrom=file3=>path136.basename(file3).replace(/\.[^.]+$/,"");function fromMarkdown(source,text3,asRules){return sectionsOf2(text3,namedFrom(source)).flatMap((s,i)=>{let key=`${source}#${i}`,name=s.name.slice(0,MAX_TYPED_CHARS).trim(),body10=scrubSecrets(s.body.trim()),boxes=boxesOf(s.body),ours=OUR_HEADING.test(s.name)||OUR_STEP.test(body10)?{out:OURS}:{};if(boxes.length>=2){let questions=boxes.slice(0,MAX_CHECKLIST_QUESTIONS).map(q4=>scrubSecrets(q4).slice(0,MAX_TYPED_CHARS));return[{key,kind:"checklist",name,source,questions,...ours}]}if(!asRules)return[];let tooLong=body10.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters). You can add a shorter one by hand.`}:{};return[{key,kind:"rule",name,source,text:body10,...tooLong,...ours}]})}function fromOurFormat(source,text3){let doc;try{doc=yaml20.load(text3)??{}}catch{return[]}return Array.isArray(doc.additions)?doc.additions.flatMap((a,i)=>{let name=typeof a?.name=="string"?a.name.trim().slice(0,MAX_TYPED_CHARS):"",key=`${source}#${i}`;return name?a.kind==="rule"&&typeof a.text=="string"?[{key,kind:"rule",name,source,text:scrubSecrets(a.text),...a.text.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters).`}:{}}]:a.kind==="checklist"&&Array.isArray(a.questions)?[{key,kind:"checklist",name,source,questions:a.questions.filter(q4=>typeof q4=="string").slice(0,MAX_CHECKLIST_QUESTIONS).map(q4=>scrubSecrets(q4))}]:a.kind==="gate"&&typeof a.command=="string"?[{key,kind:"gate",name,source,command:a.command.trim(),when:["checks","merge","release"].includes(a.when)?a.when:"checks"}]:[]:[]}):[]}function runnerFor(root){return fs144.existsSync(path136.join(root,"pnpm-lock.yaml"))?s=>`pnpm run ${s}`:fs144.existsSync(path136.join(root,"yarn.lock"))?s=>`yarn ${s}`:s=>`npm run ${s}`}function fromPackage(root,source,text3,commands){let scripts;try{scripts=JSON.parse(text3).scripts??{}}catch{return[]}let run6=runnerFor(root),own2=[commands.test,commands.lint,commands.build].filter(Boolean).join(" ");return Object.keys(scripts).filter(s=>!NEVER_A_GATE.test(s)).map(s=>({key:`${source}#${s}`,kind:"gate",name:s,source,command:run6(s),when:"checks",...OUR_SCRIPT.test(s)?{out:OURS}:new RegExp(`\\b${s.replace(/[^\w:-]/g,"")}\\b`).test(own2)?{out:"squad-kit already runs this with the checks."}:{}}))}function lookInFolder(folder,have,commands){let root;try{if(root=fs144.realpathSync(path136.resolve(folder)),!fs144.statSync(root).isDirectory())throw new Error("not a folder")}catch{throw new Error("squad-kit can't open that folder. Check the path and try again.")}let finds=[],left=0,seen=0,looked3=0,more3=!1,walk3=(dir,depth)=>{let entries;try{entries=fs144.readdirSync(dir,{withFileTypes:!0})}catch{left+=1;return}for(let e of entries.sort((a,b)=>a.name.localeCompare(b.name))){if((looked3+=1)>MAX_FOLDER_FILES*50&&(more3=!0),more3)return;let at3=path136.join(dir,e.name),source=path136.relative(root,at3).split(path136.sep).join("/");if(HOLDS_KEYS.test(e.name)||NOT_LOOKED_IN.has(e.name)){left+=1;continue}let real2=at3;if(e.isSymbolicLink()){try{real2=fs144.realpathSync(at3)}catch{real2=""}if(!real2||!`${real2}${path136.sep}`.startsWith(`${root}${path136.sep}`)){left+=1;continue}}let stat;try{stat=fs144.statSync(real2)}catch{left+=1;continue}if(stat.isDirectory()){depth<DEEPEST&&!e.isSymbolicLink()&&walk3(at3,depth+1);continue}if(!stat.isFile())continue;let isScriptCheck=source==="scripts/check",isMarkdown=/\.(md|mdc|markdown)$/i.test(e.name),isOurs=source==="harness.yaml"||source===".squad/harness.yaml";if(!isMarkdown&&!isOurs&&!isScriptCheck&&source!=="package.json"&&!(/(^|\/)rules\//.test(source)&&/\.txt$/i.test(e.name)))continue;if((seen+=1)>MAX_FOLDER_FILES){more3=!0;return}let text3=null;try{text3=textOf(real2)}catch{}if(text3===null){left+=1;continue}isOurs?finds.push(...fromOurFormat(source,text3)):source==="package.json"?finds.push(...fromPackage(root,source,text3,commands)):isScriptCheck?finds.push({key:source,kind:"gate",name:"scripts/check",source,command:"./scripts/check",when:"checks"}):finds.push(...fromMarkdown(source,text3,/^(AGENTS|CLAUDE)\.md$/.test(e.name)||/(^|\/)rules\//.test(source)))}};walk3(root,0);let named=new Set(have.map(a=>`${a.kind}\0${a.name.trim().toLowerCase()}`));return{name:path136.basename(root),path:root,finds:finds.map(f=>named.has(`${f.kind}\0${f.name.trim().toLowerCase()}`)?{...f,already:!0}:f).slice(0,MAX_FOLDER_FILES),left,more:more3}}function additionOf(f,pick2){return f.kind==="rule"?{kind:"rule",name:f.name,agents:[pick2.agent||"all"],text:f.text??""}:f.kind==="checklist"?{kind:"checklist",name:f.name,agent:pick2.agent==="tester"?"tester":"reviewer",questions:f.questions??[]}:{kind:"gate",name:f.name,command:f.command??"",when:pick2.when??f.when??"checks",on_fail:pick2.on_fail??"stop",...pick2.part?{part:pick2.part}:{}}}var SEEN_DAYS=30,WEEK_DAYS=7,ONE_DAY=864e5,STOP_LINES={checks:/^Checks failed: /,weakened:/^Stopped before the checks: the tests were made weaker/,"no-tests":/^Not checked: .*Asked you once/,guard:/^A check (needed a file outside|tried to use Docker)/,"test-change":/^squad-kit keeps the current way of testing/,review:/^(Asked for \d+ fix|Still asks for )/,proof:/^(Could not take the screenshots|No screenshots yet)/,"bad-proof":/^The screenshots did not pass squad-kit's check/,"not-shown":/^(The screenshots do not show .* clearly yet|Could not show .* on a screen after)/,broken:/^(The screenshots show .* not working yet|Still not working after)/,window:/^The \w+ asked .*squad-kit refused\.$/,questions:/did not bother you with it\.$|^The \w+ asked a technical question/,sandbox:/^(Reaching|Reading|Writing) .* was blocked: |^A download from \S+ was not allowed for this project/,lane:/^The (Builder|Tester|Reviewer|project's checks) changed .*squad-kit put |^Stopped the \w+: /,hook:/^The project's own check said no/,merge:/^(Could not merge into |It clashes with newer changes)|clashes with newer changes in \S+, so it left/,cap:/^Paused: /,budget:/^Paused before the next step/},PASSED={checks:t=>t.gates?.checks==="pass"?lineAt(t,/^All checks pass/):void 0,"test-change":t=>lineAt(t,/^squad-kit keeps the new way of testing/),review:t=>t.gates?.review==="pass"?lineAt(t,/^Approved: /):void 0,proof:t=>t.gates?.proof==="pass"&&t.proof?.length?t.history?.filter(h=>h.step==="proof").at(-1)?.to??t.activity[0]?.at:void 0,merge:t=>t.mergedAt},DID_LINES={spec:[/^Approved the spec/,"You approved"],plan:[/^Approved the (design|plan)/,"You approved"],"looks-right":[/^Checked the proof\. Looks right/,"You accepted"],ask:[/^Asked you: /,"Asked on"],chat:[/^Started talking it through/,"You talked on"],decisions:[/\. Decision saved as /,"Saved decisions on"],depends:[/so the build waits for you to choose/,"Waited for you on"],change:[/^Approved change \S+ \(big\)/,"You approved a big change on"],"reviewer-floor":[/as strong as the Builder\)$/,"Raised the model on"],"also-changed":[/^Also changed, not in the plan/,"Named something on"],"off-screen":[/^"Done when" \d+ is not on a screen/,"Marked on"],"api-proof":[/^squad-kit asked .* and kept the answer/,"Used on"],"take-over":[/^Took it over/,"You took over"],"pull-request":[/^Opened pull request/,"Opened on"],checkout:[/^It goes in once your own copy/,"Waited on"],release:[/^Released in /,"Sent out"],watchdog:[/^Looks stuck: /,"Flagged"],"step-up":[/^Runs on .*after \d+ failed tr/,"Moved up on"],"plain-words":[/^The Tester wrote something technical/,"Kept words out of the proof on"],interrupted:[/^Carried on.* after (a restart|the computer slept)|^The computer slept|^Resumed after sleep/,"Carried on after it on"],blips:[/^The connection or the AI service had a problem/,"Waited on"],leftovers:[/^Stopped (a program|\d+ programs) left running/,"Stopped programs on"]},HIT_LINES={MAX_CHECK_TRIES:/^Checks failed: .*\(try (\d+) of \1\)$/,MAX_REVIEW_ROUNDS:/^Still asks for /,MAX_RETAKES:/^Could not show .* on a screen after \d+ more tries/,MAX_PROOF_ROUNDS:/^Still not working after /,MAX_BASE_TRIES:/came in \d+ times, so it waits for you$/,REFUSALS_TO_STOP:/^Stopped: .*kept asking for things it may not do/,MISTAKES_TO_STOP:/^Stopped: .*kept using squad-kit's tools the wrong way/,TECHNICAL_TRIES:/^Stopped: .*kept asking a technical question/,BEFORE_SHOTS_MS:/^Stopped the before screenshots after/},WAIT_ROW={questions:"questions",spec:"spec",plan:"plan",proof:"looks-right",release:"release",ask:"ask",yours:"take-over",chat:"chat",change:"change",depends:"depends"},lineAt=(t,re)=>t.activity.find(a=>re.test(a.text))?.at,stopRow=s=>s.kind??s.gate??(s.budget?"budget":"ways-out"),overrideRow=o=>o.added?"gate-cant-run":o.weakened?.length?"weakened":o.gate==="hooks"?"hook":o.gate;function needRow(t){let gate=t.waiting?.kind==="stuck"?t.stuck?.added?.find(g=>g.id!==HARNESS_FILE_GATE):void 0;return gate?`add:${gate.id}`:t.waiting?t.waiting.kind==="stuck"?t.stuck?stopRow(t.stuck):"ways-out":WAIT_ROW[t.waiting.kind]:t.quiet?.kind==="stuck"?"watchdog":t.outdated?.length?"decisions":t.testChange?.ask?"test-change":"change"}var storyWord=n=>`${n} ${n===1?"story":"stories"}`;function harnessSeen(bp,meta,now2=Date.now()){let from=now2-SEEN_DAYS*ONE_DAY,week=now2-WEEK_DAYS*ONE_DAY,fresh=at3=>!!at3&&Date.parse(at3)>=from,rows2={},weekIds={},hits={},did={},said={},tickets=listTickets(bp).filter(t=>!t.deleting),counted=0,wentPast=0;for(let t of tickets){let lines3=t.activity.filter(a=>fresh(a.at));(lines3.length||fresh(t.createdAt))&&(counted+=1);let stops=new Map,stop=(row3,at3)=>{fresh(at3)&&!(stops.get(row3)>at3)&&stops.set(row3,at3)};for(let[row3,re]of Object.entries(STOP_LINES))stop(row3,lines3.find(a=>re.test(a.text))?.at);t.stuck&&t.waiting?.kind==="stuck"&&stopRow(t.stuck)!=="ways-out"&&stop(stopRow(t.stuck),t.activity[0]?.at??t.createdAt);for(let o of t.overrides??[])stop(overrideRow(o),o.at);for(let r of t.added??[])r.stopped&&stop(`add:${r.id}`,r.stopped);t.overrides?.some(o=>fresh(o.at))&&(wentPast+=1);for(let[row3,at3]of stops){let r=rows2[row3]??={};r.stopped=(r.stopped??0)+1,Date.parse(at3)>=week&&(r.week=(r.week??0)+1,(weekIds[row3]??=[]).push(t.id)),(!r.last||at3>r.last.at)&&(r.last={id:t.id,title:t.title,at:at3})}for(let[row3,at3]of Object.entries(PASSED).map(([row4,of])=>[row4,of(t)]))!stops.has(row3)&&fresh(at3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1);for(let r of(t.added??[]).filter(x=>fresh(x.at))){let row3=`add:${r.id}`,s=said[row3]??={would:0,warned:0},no=r.answers?.some(a=>a.answer==="no");(r.result==="would-stop"||no&&r.state==="try")&&(s.would+=1),r.result==="warned"&&(s.warned+=1),r.result==="cant-run"&&!(s.cant?.at>r.at)&&(s.cant={id:t.id,title:t.title,at:r.at}),(r.result==="passed"||r.answers?.length&&r.answers.every(a=>a.answer==="yes"))&&!stops.has(row3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1)}for(let[row3,[re]]of Object.entries(DID_LINES))lines3.some(a=>re.test(a.text))&&(did[row3]=(did[row3]??0)+1);for(let[limit,re]of Object.entries(HIT_LINES)){let at3=lines3.find(a=>re.test(a.text))?.at;at3&&!(hits[limit]?.at>at3)&&(hits[limit]={id:t.id,title:t.title,at:at3})}}for(let row3 of Object.keys(PASSED))rows2[row3]&&(rows2[row3]={passed:0,stopped:0,...rows2[row3]});for(let[row3,s]of Object.entries(said)){let words2=[s.would?`Would have stopped ${storyWord(s.would)}`:"",s.warned?`Warned on ${storyWord(s.warned)}`:"",s.cant?`Could not run on ${s.cant.id}`:""].filter(Boolean);rows2[row3]={passed:0,stopped:0,...rows2[row3],...words2.length?{did:words2.join(". ")}:{}}}for(let[row3,n]of Object.entries(did))(rows2[row3]??={}).did=`${DID_LINES[row3][1]} ${storyWord(n)}`;wentPast&&((rows2["ways-out"]??={}).did=`You went past a check on ${storyWord(wentPast)}`);let pause2=readPause();pause2&&pause2.project===meta.project&&fresh(pause2.since)&&((rows2["usage-limit"]??={}).did=`Last waited ${pause2.since.slice(0,10)}`);let needs=inbox(bp).filter(t=>!t.deleting&&!t.archived).map(t=>({id:t.id,summary:t.waiting?.summary??t.quiet?.detail??t.status,row:needRow(t),proof:t.waiting?.kind==="proof"}));return{stories:counted,rows:rows2,weekIds,hits,needs}}function lineStages(){let you=sub=>({kind:"you",sub}),squad=sub=>({kind:"squad",sub}),skip=sub=>({kind:"skip",sub}),on=(full,express=full,auto=full)=>({full,express,auto}),stage=(id2,name,about,when2,gates)=>({id:id2,name,about,on:when2,gates}),g=(id2,name,what,fail,more3)=>({id:id2,name,what,...fail?{fail}:{},...more3}),y=(id2,name,what,fail,more3)=>g(id2,name,what,fail,{you:!0,...more3}),ON_ITS_OWN=["express","auto"],floor=QUALITIES.filter(q4=>QUALITY_PRESETS[q4].reviewerFloor).map(q4=>q4.charAt(0).toUpperCase()+q4.slice(1)).join(" and ");return[stage("setup","Setup","Once for each project, before any story.",on(you("You confirm")),[g("tool-ready","Your AI tool is here","It must be installed, or nothing runs.","It says what to do."),g("first-run","A first run on untouched code","Your tests and app run once in squad-kit's own copy. What already fails is noted, so the Builder leaves other screens alone.","The setup goes on. A story must still pass its checks."),y("overview","You confirm what it found","The Architect explains your project in plain words.","You say what is wrong, and it reads again."),g("project-rules","Your project's own rules","Every agent first reads your rule files and your team's wiki. squad-kit runs the process, and those files cover the code.")]),stage("breakdown","Breakdown","Your requirements become a tree of stories. Skip it if you start from one story.",on({...skip("Optional"),off:!0},{...skip("Optional"),off:!0},you("You approve")),[g("brd-questions","Only real gaps and clashes",`At most ${MAX_QUESTIONS} questions on a document, where it leaves a gap or disagrees with itself.`,"A technical one goes back to the agent."),y("brd-approve","You approve the breakdown",'Nothing is built or sent to your tracker before that. Every story needs its "done when".',"Approve is refused and says what to fix."),g("brd-queue","Stories start in order","The next starts when what it needs is merged and a place is free.","Your budget, your AI limit or two stopped stories hold the queue, with a reason."),g("first-story","A project with no code starts with one story","squad-kit adds Set up the product: it picks the tools and makes a first version. Nothing else starts until it merges, so every story builds on the same start.","Another ticket waits, and says which one it waits for.")]),stage("story","Story","The Analyst reads your story and asks only what it leaves open.",on(you("You answer"),skip("No questions"),skip("Approved before")),[g("questions","Only business questions reach you","The Analyst asks about gaps and clashes. Asks about settings, secrets or code history are refused before you see them.",void 0,{skip:ON_ITS_OWN}),y("spec","You approve what gets built",'The story and its "done when" list wait for you.',"You ask for a change and it is rewritten.",{skip:ON_ITS_OWN,note:{auto:"Approved with the breakdown."}}),y("ask","An agent can stop and ask you","The Architect, Builder or Reviewer can ask one plain question. It passes the same filter.",void 0,{note:{auto:"Only for a critical gap or a clash. Small choices go under Decided for you."}}),y("chat","Talk it through","With any agent that worked on the story. It waits while you talk.")]),stage("plan","Plan","The Architect reads your project and writes the plan in plain words.",on(you("You approve"),skip("On its own"),skip("On its own")),[y("plan","You approve the plan","Nothing is built before you approve how.","You ask for a change and it is rewritten.",{skip:ON_ITS_OWN}),g("decisions","Saved decisions stay rules","Each plan says which ones it follows or replaces."),y("depends","Stories that depend on each other","A story that needs unmerged work waits: build on it, build anyway, or wait."),y("change","A big change comes back to you","The Builder pauses, and the updated design waits for your yes.")]),stage("build","Build","The Builder writes the code in small steps, in a locked room.",on(squad("Builder")),[g("sandbox","The guard","Each story has its own copy. No agent can read your keys or sign-ins. The Builder and Tester can't read .env files either, and reach only the websites the project's network level allows.","Blocked, with a line on the story. A website it may not reach stops nothing: the work goes on without it."),g("guard","A second guard for your project","Your tests, app, setup and release run locked in too, with keys hidden and Docker closed.","A card: an engineer allows the file, or the plan changes."),g("lane","Files no agent can change","squad-kit's folder, AI instruction files, commit hooks, files an engineer locked. No commit or branch change, and a push is refused.","A file goes back, with a line. A moved branch stops the step."),g("local-files","Test values, not your own","A file marked local gets values kept for stories. No agent can read it."),y("new-tests","New tests",`The Builder writes tests for every "done when". On a new story you can turn off Write new tests: your project's own checks, the review and the proof still run, and the story says "Built without new tests". An engineer can keep it on for every story.`,void 0,{note:{auto:"Always on: a story from the breakdown writes its tests."}})]),stage("checks","Checks","squad-kit runs your project's own checks. No agent is asked if they passed.",on(squad("squad-kit")),[g("checks","Your project's own checks","Test, lint and build, plus scripts/check when your project has one. The Builder's word doesn't count. A failed check runs once more, so a flaky test doesn't stop the story. For the tests, the failed ones run on their own first: one that still fails fails the check at once, and if they pass, all the tests run once more and decide.",`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`),g("setup-again","Set up again when the code changes","New packages or database changes came into a story's copy, from main or from the Builder: its setup runs again before the checks and before the app starts, so the app never runs on an old database.","A database of the story's own that is out of step is made again, and the setup runs once more. Then the Builder fixes it."),g("old-tests","An old bug is not the story's","A failed test in a file the story didn't change runs on its own on main's code. If it fails there too, it is an old bug: the story goes on, the checks say so, and squad-kit makes one new story to fix that test, which waits on the board until you start it. A story only goes past a test that main already fails.","If it passes on main, the Builder fixes it, as for any failed check."),g("weakened","Weakened tests","Tests deleted, switched off or reduced, test settings changed, or a change to what a must-pass gate of your team runs.","It stops. Only you can go past it, with a reason on the record."),g("no-tests","No tests, no green","No tests never means a green pass. Lint, build and the app starting must still pass.","Asked once per project: add tests, or go on without."),g("test-change","Test commands stay strong","A new test command stays only if it passes and runs at least as many tests.","If squad-kit can't tell, you choose."),g("gate-cant-run","A check that can't run","One of your team's gates that can't run, or a file of additions that can't be read, says nothing about the code. It never counts against the story, and it is never dropped without a word.","It retries by itself, then a card: try again, let an engineer look, or go on with a reason. On Builds by itself it waits for a person.")]),stage("review","Review","A Reviewer that never saw the build reads the Builder's work.",on(squad("Reviewer")),[g("review","A fresh Reviewer",'It checks every "done when" and saved decision, and looks for bugs, security holes and missing or weakened tests. It changes nothing.',`Back to the Builder, up to ${MAX_REVIEW_ROUNDS} rounds. Then you decide.`),g("reviewer-floor","Never weaker than the Builder",`On ${floor}, its model is at least as strong.`),g("also-changed","Also changed","It names what the change touches beyond the story and plan.","It doesn't block. It shows at Your check.")]),stage("proof","Proof",'The Tester runs your real app and shows each "done when" working.',on(squad("Tester")),[g("proof","Pictures of the real app",`Up to ${6} for each "done when".`,"None: a card, and you decide."),g("bad-proof","squad-kit checks each picture",`Real, at least ${MIN_SIZE.width} by ${MIN_SIZE.height}, not blank, taken in this step.`,"Back to the Tester once. Then a card."),g("not-shown",'A second look at every "done when"',"A separate run judges its pictures: met, not working, not shown, or not on a screen.",`Not shown: up to ${2} retakes. Then a card.`),g("broken","Not working yet","Pictures that show it does not work never pass.",`Back to the Builder ${"once"}. Then a card.`),g("off-screen","Not on a screen",'Takes two voices: the Tester and the check. If every "done when" ends that way, you decide.'),g("api-proof","Answers from your API","Work with no screen can show a saved answer from this story's own API."),g("window","Fixed tools only","The Tester uses squad-kit's own tools, on this story's app only.",`Refused. ${REFUSALS_TO_STOP} refusals stop it.`)]),stage("yours","Your check","You look at the proof, try the app, and ask anything.",on(you("You decide"),you("You decide"),skip("On its own")),[y("looks-right","You look and decide","Where to look, Try it yourself, Ask a question, Something's wrong. Nothing merges before Looks right.","Something's wrong sends it back with your words.",{skip:["auto"],note:{auto:"It merges when the checks, review and proof pass. If not, it comes to you."}}),y("take-over","Take it over","Work in the story's copy yourself. The squad keeps off until you give it back."),y("outside","Work in another AI tool","Copy a prompt, and a conversation in any AI tool carries on from this step. The squad keeps off until it comes back. It keeps a label on its record, and Check runs squad-kit's own checks, review and proof on it. What is spent there is yours, and the guard and the limits do not apply there."),g("accept-order","First things first","A story built on another is accepted after it, and an open change is settled first.","Accept says what comes first.")]),stage("merge","Merge","The story joins the rest of your code.",on(squad("squad-kit")),[g("hook","Your check on every save","Your commit hooks run on every save, inside the guard.","The Builder fixes it, or you save without it and say why."),g("merge","A clean merge","The latest code comes in first. A clash is never forced.",`The newest code comes in by itself, up to ${MAX_BASE_TRIES} times. Then a card.`),g("pull-request","Pull requests",`squad-kit opens and merges them. Waiting for an approver is a normal wait. One merged on the code host is noticed within ${OUTSIDE_LOOK_MS/6e4} minutes, or at once with Check again.`,"Try again, or let the release merge the work."),g("checkout","Your own copy is safe","With changes in the same files, the merge waits. It never overwrites them.")]),stage("release","Release","You choose what goes out and when. Every track stops here.",on(you("You decide")),[y("release","Release stays yours","Deploying never happens by itself.","If it can't be sent, it isn't marked as sent. Send again finishes it."),y("leave-out","Leave a story out","It stays in your code and goes out with the next release."),g("release-clash","Nothing moves on a clash","Every merge is tried first. What the others rely on goes first.","A clash: one card, nothing moved. A failed part: the ones after it wait."),g("release-lock","One release at a time","One cut off by a restart is finished or rolled back on the next start.")])]}function alwaysOnRows(){let a=(id2,name,what,then)=>({id:id2,name,what,...then?{then}:{}});return[a("watchdog","Nothing hangs in silence","When a step goes quiet, the watchdog looks at what is really running, to tell busy from stuck.","You get a card with ways out."),a("usage-limit","Your AI limit is respected","At the limit every project waits together, then carries on from the same step.","Never a failed try, and you can end the wait yourself."),a("step-up","A stronger model after a failed try","The Builder moves up, unless someone chose an exact model.",`On Thrifty, after ${QUALITY_PRESETS.thrifty.stepUp.after} failed tries.`),a("cap","A run can't go on forever","Each run has a most it can use. See Limits.","It pauses with its work kept. Give it more doubles it."),a("budget","Your budget",`If you pay per use, a story and a month can have one. At ${WARN_AT*100}% the board says so.`,"At 100% the story or the Analyst stops between two steps, never mid-step."),a("ways-out","Every stop has a way out","Try again, do it yourself, go around it, or stop. Only you can go past a failed check.","Your reason stays on the story, the pull request and the release notes."),a("plain-words","Plain words only","Raw errors never reach you.","The exact error goes to the engineer details and the log."),a("interrupted","An interruption is not a failure","A restart, a Stop or a sleeping computer never counts as a failed try.","The step carries on where it was."),a("blips","A lost connection is not a failure","The step runs again by itself.","If it does not pass, you get a card."),a("leftovers","Nothing left behind","A program a step leaves running is stopped. Programs of your own are never touched.","On Windows it stops only what it started itself."),a("product-lock","One story runs the product at a time","Stories that need the whole product take turns, and say who they wait for."),a("room-for-copy","Room on the disk","Before a first copy of the code, squad-kit compares the free space with the size of the project.","Too little: a card says how much it needs and how much is free. Free up some space, then press Try again."),a("save-trouble","A full disk holds the work","If squad-kit can't save its files, the project waits and says so on every page.","It carries on once saving works."),a("board-lock","Your board is locked","It opens only on your own computer's address, with your link's key.","A request from any other website is refused."),a("report","Reports leave your secrets out",NEVER)]}function limitRows(meta){let row3=(group,show3=String)=>(limit,name,what="")=>{let[id2,n]=Object.entries(limit)[0];return{id:id2,group,n:show3(n),name,what}},tries=row3("tries"),pics=row3("pictures"),asks=row3("asks"),time2=row3("time",ms2=>`${ms2/6e4} min`),size2=row3("sizes"),bytes=row3("sizes",b=>`${b/1024/1024} MB`);return[tries({MAX_CHECK_TRIES},"Tries at the checks","Then a card."),tries({MAX_RUNS_ON_MAIN},"Runs of a failed test on main's code","It stops at the first that fails there too."),size2({MAX_ALONE},"Failed tests run on their own first","With more, all the tests run again at once."),size2({MAX_OLD},"Failed tests run on main's code","With more, the Builder fixes them, as before."),tries({MAX_REVIEW_ROUNDS},"Review rounds","Then you decide."),tries({MAX_RETAKES:2},"Retakes",`For a "done when" the pictures don't show clearly.`),tries({MAX_PROOF_ROUNDS:1},"Returns after a broken picture","To the Builder. Then you decide."),tries({MAX_BASE_TRIES},"Clash retries","While the code keeps moving."),tries({BLIP_WAITS_MIN:BLIP_WAITS_MIN.length},"Waits after a lost connection",`After ${BLIP_WAITS_MIN.join(", ")} minutes. Then a card.`),pics({MAX_PICTURES_PER_DONE_WHEN:6},'Pictures for each "done when"',"So each part can show."),pics({MAX_PICTURES:40},"Pictures in one proof step","Counting each one taken again."),pics({MAX_SHOTS_PER_CALL},"Pictures in one call","More has to be split over calls."),pics({MAX_API_CALLS},"Calls to your API in one proof","Only this story's own app."),asks({REFUSALS_TO_STOP},"Refusals","Forbidden asks, then the agent is stopped."),asks({MISTAKES_TO_STOP},"Calls in the wrong shape","Harmless, but past this the step ends."),asks({MAX_STUCK},"Times a run can say it is stuck","After that, squad-kit says it already tried."),asks({TECHNICAL_TRIES:2},"Tries for a technical question","Back to its agent, never to you. Then it stops."),asks({MAX_QUESTIONS},"Questions on one document","Only real gaps and clashes."),asks({MAX_DECIDED},"Small choices in one answer","Logged under Decided for you."),time2({WORDS_STEP_MS},"Analyst and Architect","One run of words: questions, a spec, a design."),time2({BUILD_STEP_MS},"Builder","One run of building or fixing."),time2({REVIEW_STEP_MS},"Reviewer","One round, or one look at the pictures."),time2({PROOF_STEP_MS},"Tester"),time2({BRD_READ_MS},"Reading a document","A long document gets twice its usual read."),time2({LONGEST_READ_MS},"Reading a very long document","The most, however long it is."),time2({BRD_WRITE_MS},"Writing a breakdown"),time2({CHECK_MS},"One check"),time2({CHECK_SILENT_MS},"A silent check","It prints nothing for this long."),time2({APP_WAIT_MS},"Waiting for your app"),time2({TURN_WAIT_MS},"Tests and your app in one copy","One waits for the other, then it stops and says so."),time2({KEPT_APP_MS},"Your app kept for the proof","After the review, while nothing uses it. Then it stops."),time2({SETUP_MS},"Setting up your app"),time2({DOCKER_WAIT_MS},"Starting in Docker"),time2({BEFORE_SHOTS_MS},"Before pictures","Beside the build, holding nothing up."),time2({ON_MAIN_MS},"Failed tests on main's code","All their runs together. Then the Builder fixes them, as before."),time2({HOOK_MS},"Your check on a save"),time2({NETWORK_GIT_MS},"Reaching your code host","One push, pull or fetch."),time2({PIPELINE_MS},"Your after-release step","Inside the guard, after a release."),time2({MAX_LANDING_WAIT_MS},"Waiting for a story to merge","Before the newest code comes in."),time2({OUTSIDE_LOOK_MS},"Looking for merges on your code host","Pull requests merged there, outside squad-kit. Check again looks now."),time2({WAIT_LIMIT_MS},"Waiting for the product"),time2({TRY_YIELDS_AFTER_MS},"Trying a story yourself","Once another story needs it."),time2({QUIET_MS},"Quiet before a look","Then the watchdog looks."),time2({LONG_MS},"Busy before Taking long","Working, with no progress."),time2({RETRY_MS},"Looking again at your AI limit","When your AI tool doesn't say when it ends."),size2({parallel:parallelLimit(meta?.parallel)},"Stories at once","Across your projects. Change it in Settings."),size2({MAX_PARTS},"Parts of one product"),size2({MAX_UPLOADS},"Files on one story"),size2({MAX_RULES},"Rules your team can add","Remove one to add another."),size2({MAX_RULE_CHARS},"Characters in one rule","Agents read every rule at every step, so each stays short."),size2({MAX_CHECKLIST_QUESTIONS},"Questions on one checklist"),size2({MAX_TYPED_CHARS},"Characters in a name, a question or a command"),size2({MAX_FOLDER_FILES},"Files looked at in a folder you bring in","The rest are left out, and the sheet says so."),row3("sizes",b=>`${b/1e3} KB`)({MAX_FOLDER_FILE_BYTES},"Largest file read in that folder","A bigger one is left out."),bytes({MAX_UPLOAD_BYTES},"One file"),bytes({MAX_NOTE_BYTES},"One voice or video note")]}var HARNESS_CAP_NAME={analyst:"Analyst",architect:"Architect",builder:"Builder","builder.fix":"Builder, fixes",reviewer:"Reviewer",tester:"Tester",chat:"Talking a story through"},HARNESS_PRIVATE_NAME={".ssh":"SSH keys",".aws":"AWS sign-in",".gnupg":"GPG keys",".config/gh":"GitHub sign-in",".azure":"Azure sign-in",".kube":"Kubernetes sign-in",".docker/config.json":"Docker sign-in",".netrc":"Saved website passwords",".git-credentials":"Saved git passwords"},HARNESS_REFUSED={settings:"Change a squad-kit setting or a protected file",secret:"Hand over a password or a key",history:"Change the history of the code"},HARNESS_TOOL_NAME={take_screenshots:"Take pictures",look_at_page:"Look at a page",start_app:"Start the app",stop_app:"Stop the app",app_status:"See if the app runs",run_checks:"Run the project's checks",add_part:"Add a part of the product",ask_api:"Ask your API",i_am_stuck:"Say it is stuck"};function levelSites(level){return level==="off"?["Only this computer"]:level==="standard"?["Any website but the closed ones"]:RECOMMENDED_GROUPS.map(g=>g.name)}function agentPowers(){let yes=label3=>[label3,!0],no=label3=>[label3,!1],reads=[yes("Read your project"),no("Change code"),no("Run commands"),no("No internet")];return{analyst:{can:reads},architect:{can:reads},builder:{can:[yes("Read your project"),yes("Change code"),yes("Run commands"),yes("The websites your network level allows"),yes("squad-kit's tools: pictures, checks, the app")]},reviewer:{can:[...reads,yes("Look at a page of this story's app")]},tester:{can:[...reads,yes("squad-kit's tools: pictures, the app, your API")]}}}function rowDetails(meta,root){let min=ms2=>`${ms2/6e4} min`,repos=meta?reposOf(meta):[],run6=(...keys)=>repos.map(r=>[repos.length>1?`${r.path}: `:"",keys.map(k=>r.commands[k]).filter(Boolean).join(", ")]).filter(([,c])=>c).map(([p,c])=>`${p}${c}`).join("; ")||"Not set yet",guard=guardSupport(),tools4=sandboxStatus(),level=parseNetwork(meta?.network)??DEFAULT_NETWORK,state=(on,why)=>`${on?"On":"Off"}: ${why}`,names2=list2=>list2?.map(f=>f.path).join(", ")||"None yet",tab=name=>`The story's ${name} tab`,code=(file3,...more3)=>[...more3,["In the code",`src/${file3}`]];return{"tool-ready":code("engine/onboard.ts",["Found",meta?.tools?.map(t=>`${t.name}: ${t.ready?"ready":"not found"}`).join(", ")||"Not looked yet"]),"first-run":code("engine/onboard.ts",["Runs",run6("setup","test","start")],["See it in","The Overview page"]),overview:code("board/first-run.ts",["See it in","The Overview page"]),"gate-cant-run":code("engine/gate-run.ts",["File",".squad/harness.yaml"]),"project-rules":code("engine/repos.ts",["Reads",(root?rulesFiles(root).map(f=>f.slice(root.length+1)):[]).join(", ")||"None found yet"]),"brd-questions":code("engine/breakdown-job.ts",["Limit",`${MAX_QUESTIONS} questions`]),"brd-approve":code("board/requirements-actions.ts",["See it in","The Requirements page"]),"brd-queue":code("board/requirements-build.ts",["Places",`${parallelLimit(meta?.parallel)} at once`]),"first-story":code("board/first-story.ts"),questions:code("engine/question-filter.ts",["Refused",Object.values(HARNESS_REFUSED).join(", ")],["Tries",`${2} back to its agent`]),spec:code("board/actions.ts",["See it in",tab("Spec")]),ask:code("engine/live.ts"),chat:code("board/actions.ts"),plan:code("board/actions.ts",["See it in",tab("Design")]),decisions:code("board/actions.ts",["See it in","The Architecture page"]),depends:code("board/depends.ts"),change:code("board/actions.ts"),sandbox:code("engine/sandbox.ts",["On this computer",tools4.map(t=>state(t.available,t.why)).join(", ")||"Off"],["Network level",NETWORK_NAME[level]],["Also allowed",meta?.allowedHosts?.join(", ")||"None yet"],["Lists","src/engine/network-lists.ts"]),guard:code("engine/project-guard.ts",["On this computer",state(guard.on,guard.why)],["Network",networkPolicy(level).commands==="this-computer"?"Only this computer (Off)":"Open"],["Also allowed",[...meta?.guard?.allowRead??[],...meta?.guard?.allowWrite??[]].join(", ")||"None yet"]),lane:code("engine/lane.ts",["Locked here",names2(meta?.lockedFiles)]),"local-files":code("engine/local-files.ts",["Local here",names2(meta?.localFiles)]),"new-tests":code("engine/prompts.ts",["Here",meta?.alwaysNewTests?"Always on: every story writes new tests":"Your choice on each new story, on by default"],["Set in","Settings"]),checks:code("engine/checks.ts",["Runs",run6("test","lint","build")],["Limit",`${MAX_CHECK_TRIES} tries, ${min(CHECK_MS)} each, ${min(CHECK_SILENT_MS)} with no output`],["Decided by","The exit code, never an agent"],["See it in",tab("Developer")]),"old-tests":code("engine/old-tests.ts",["Runs",run6("test")],["Limit",`${MAX_OLD} failed tests, each up to ${MAX_RUNS_ON_MAIN} times, ${min(ON_MAIN_MS)} in all`],["Reads","Playwright, Vitest, Jest, pytest and go test"],["See it in","The proof page and the Releases page"]),weakened:code("engine/weakened.ts",["Looks at","Deleted tests, skip marks, fewer tests, test settings"],["See it in",tab("Changes")]),"no-tests":code("engine/not-checked.ts",["Here",meta?.noTests?"You chose to go on without tests":"Not asked yet"]),"setup-again":code("engine/live.ts",["Runs",run6("setup")],["Looks at","Package files and database changes, saved or not"]),"test-change":code("engine/test-change.ts",["Runs",run6("test")],["Compares","Pass or fail, and how many tests ran"]),review:code("engine/live.ts",["Limit",`${MAX_REVIEW_ROUNDS} rounds, ${min(REVIEW_STEP_MS)} each`],["See it in",tab("Conversation")]),"reviewer-floor":code("engine/levels.ts"),"also-changed":code("board/also-changed.ts",["Blocks","Nothing"]),proof:code("engine/live.ts",["Runs",run6("start")],["Limit",`${6} for each, ${40} in one step, ${min(PROOF_STEP_MS)}`]),"bad-proof":code("engine/proof-check.ts"),"not-shown":code("engine/proof-verdict.ts",["Limit",`${2} retakes`]),broken:code("engine/proof-verdict.ts",["Limit",`${2} rounds`]),"off-screen":code("engine/proof-verdict.ts"),"api-proof":code("engine/window.ts",["Limit",`${MAX_API_CALLS} calls in one proof`]),window:code("engine/window.ts",["Limit",`${REFUSALS_TO_STOP} refusals, ${MISTAKES_TO_STOP} calls in the wrong shape`]),"looks-right":code("board/actions.ts",["See it in","The proof page"]),"take-over":code("board/actions.ts"),outside:code("board/outside.ts",["File",".squad/how-to-work-here.md"]),"accept-order":code("board/depends.ts"),hook:code("engine/git.ts",["Limit",min(HOOK_MS)]),merge:code("board/bring-in.ts",["Limit",`${MAX_BASE_TRIES} tries`]),"pull-request":code("engine/sync.ts",["Limit",`${min(NETWORK_GIT_MS)} to reach your code host`]),checkout:code("board/auto.ts"),release:code("engine/release.ts",["Runs",run6("deploy")],["Limit",min(PIPELINE_MS)]),"leave-out":code("engine/leave-out.ts"),"release-clash":code("engine/release.ts"),"release-lock":code("board/lock.ts"),"room-for-copy":code("engine/git.ts"),watchdog:code("engine/watchdog.ts",["Limit",`Looks after ${min(QUIET_MS)} quiet, says taking long after ${min(LONG_MS)}`]),"usage-limit":code("engine/usage-limit.ts",["Looks again",`Every ${min(RETRY_MS)} when the tool does not say when`]),"step-up":code("engine/levels.ts"),cap:code("engine/caps.ts"),budget:code("board/budget.ts",["Set in","Settings"]),"ways-out":code("board/actions.ts"),"plain-words":code("engine/plain-stop.ts"),interrupted:code("engine/interrupt.ts"),blips:code("engine/live.ts",["Waits",`${BLIP_WAITS_MIN.join(", ")} min`]),leftovers:code("engine/leftovers.ts"),"product-lock":code("engine/product-lock.ts",["Limit",min(WAIT_LIMIT_MS)]),"save-trouble":code("engine/save-trouble.ts"),"board-lock":code("console/auth.ts"),report:code("engine/report.ts")}}function trackCell(stage,g,track2){let note2=g.note?.[track2]?{note:g.note[track2]}:{};return g.skip?.includes(track2)?{word:"By itself",...note2}:stage.on[track2].off?{word:"Not used"}:{word:g.you?"You":"Runs",...note2}}var RULE_STAGE={all:"build",analyst:"story",architect:"plan",builder:"build",reviewer:"review",tester:"proof"},additionStage=a=>a.kind==="gate"?a.when:a.kind==="checklist"?a.agent==="reviewer"?"review":"proof":RULE_STAGE[a.agents.length===1?a.agents[0]:"all"];function additionRow(a){let by2=[["Added by",a.added_by],["From",a.from==="typed"?"Typed here":`The folder ${a.from}`]],team={state:a.state,kind:a.kind},row3={id:`add:${a.id}`,name:a.name,team};if(a.kind==="rule")return{...row3,what:a.text,d:by2};if(a.kind==="checklist"){let fail2=a.state==="try"?"The answers show on the story. A no stops nothing.":a.agent==="reviewer"?"A no counts as a review finding.":'A no goes the same way as a "done when" not shown.';return{...row3,what:`The ${a.agent==="reviewer"?"Reviewer":"Tester"} answers: ${a.questions.join(" ")}`,fail:fail2,d:by2}}let fail=a.state==="try"?'It is noted as "would have stopped". Nothing stops.':a.on_fail==="warn"?"It is noted. Nothing stops.":{checks:`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`,merge:"The change is not saved, and a card has the ways out.",release:"The release does not go out, and the page has the ways out."}[a.when];return{...row3,what:"A command squad-kit runs itself. No agent is asked if it passed.",fail,d:[["Command",a.command],...a.part?[["Part",a.part]]:[],...by2]}}function harnessView(meta,bp){let quality=meta?.quality??DEFAULT_QUALITY,caps=QUALITY_PRESETS[quality].caps,seen=meta&&bp?harnessSeen(bp,meta):void 0,details=rowDetails(meta,bp?.root),addedOn=new Set(bp?readHarness(bp).additions.filter(a=>a.state!=="off").map(a=>`add:${a.id}`):[]),waiting2=seen?.needs.map(n=>n.row.startsWith("add:")&&!addedOn.has(n.row)?{...n,row:"gate-cant-run"}:n),fill2=r=>{let needs=waiting2?.filter(n=>n.row===r.id);return{...r,d:details[r.id]??r.d??[],...seen?.rows[r.id]?{seen:seen.rows[r.id]}:{},...needs?.length?{needs}:{}}},file3=bp?readHarness(bp):{additions:[],log:[]},live=file3.additions.filter(a=>a.state!=="off"),addedCell=a=>({word:"Runs",...a.state==="try"?{note:"Try first"}:{}}),stages=lineStages().map(s=>{let gates=[...s.gates.map(g=>({...fill2(g),cells:{full:trackCell(s,g,"full"),express:trackCell(s,g,"express"),auto:trackCell(s,g,"auto")}})),...live.filter(a=>additionStage(a)===s.id).map(a=>({...fill2(additionRow(a)),cells:{full:addedCell(a),express:addedCell(a),auto:addedCell(a)}}))];return{...s,gates,stopped:new Set(gates.flatMap(g=>seen?.weekIds[g.id]??[])).size}}),always=alwaysOnRows().map(fill2),rows2=[...stages.flatMap(s=>s.gates),...always],offHere=(sandboxStatus().some(t=>t.available)?0:1)+(guardSupport().on?0:1),level=parseNetwork(meta?.network)??DEFAULT_NETWORK;return{quality,qualityNote:QUALITY_NOTE,range:{days:SEEN_DAYS,weekDays:WEEK_DAYS,stories:seen?.stories??0},tiles:{working:rows2.length-offHere,stopped:rows2.filter(r=>r.seen?.week).length,needs:seen?.needs??[]},tracks:[{id:"full",name:"Full",words:"On Full you answer the questions and approve the plan."},{id:"express",name:"Express",words:"On Express there are no questions and the plan is approved on its own. You still check the work."},{id:"auto",name:BUILDS_BY_ITSELF,words:"Approved in advance, it merges when the checks, the review and the proof pass. Release stays yours."}],stages,alwaysOn:always,limits:limitRows(meta).map(l=>seen?.hits[l.id]?{...l,hit:seen.hits[l.id]}:l),caps:Object.keys(HARNESS_CAP_NAME).map(key=>({key,name:HARNESS_CAP_NAME[key],...caps[key]})),agents:agentPowers(),additions:{items:file3.additions.map(a=>({...a,stage:additionStage(a),...seen?.rows[`add:${a.id}`]?{seen:seen.rows[`add:${a.id}`]}:{},...a.kind==="rule"&&ruleNote(a.text)?{note:ruleNote(a.text)}:{},...a.kind==="gate"&&a.part&&meta&&!partsOf(meta).some(p=>p.id===a.part)?{partGone:!0}:{}})),log:file3.log.slice(-200).reverse(),..."broken"in file3&&file3.broken?{broken:file3.broken}:{},parts:meta&&severalParts(meta)?partsOf(meta).filter(p=>!p.shared).map(p=>({id:p.id,name:p.name})):[],guarded:guardSupport().on,demo:meta?.engine!=="live",shared:bp?docsInGit(bp):!1,limits:{rules:MAX_RULES,ruleChars:MAX_RULE_CHARS,questions:MAX_CHECKLIST_QUESTIONS}},safety:{hidden:[...PRIVATE.map(p=>HARNESS_PRIVATE_NAME[p]??p),"Your AI tools' sign-ins","Browser sign-ins","Keys in package settings",".env files","squad-kit's own folder and keys"],locked:["squad-kit's own folder","AI instruction files","Commit hooks and their settings","Code history: commit and branches. A push is refused, and agents have no keys to push with"],sites:levelSites(level),network:{level,levels:NETWORK_LEVELS.map(id2=>({id:id2,name:NETWORK_NAME[id2],words:NETWORK_WORDS[id2]})),closed:KNOWN_BAD_GROUPS.map(g=>g.name),commands:networkPolicy(level).commands},refused:Object.values(HARNESS_REFUSED),tools:Object.values(HARNESS_TOOL_NAME),local:(meta?.localFiles??[]).map(f=>f.path),lockedByYou:(meta?.lockedFiles??[]).map(f=>f.path)}}}var ARABIC=/[؀-ۿݐ-ݿࢠ-ࣿﭐ-﷿ﹰ-]/g;function isArabic(text3){let ar=text3.match(ARABIC)?.length??0,la=text3.match(/[A-Za-z]/g)?.length??0;return ar>0&&ar>=la}function plainArabic(text3){return text3.replace(/[ً-ْٰـ]/g,"").replace(/[أإآٱ]/g,"\u0627").replace(/ة/g,"\u0647").replace(/ى/g,"\u064A").replace(/ؤ/g,"\u0648").replace(/ئ/g,"\u064A")}var EN_POLITE=/^(please\s+)?((can|could|would) you\s+)?(please\s+)?(tell me|let me know|show me|do you know|i want to know|i'd like to know)\s+/,EN_DO=/^(make|change|add|remove|delete|use|put|rename|move|show|hide|replace|set|turn|fix|update|switch|include|drop|give|let|don't|dont|do not|never|always|write|create)\b/,EN_ASK=/\b(i want|i'd like|i would like|we need|we want|it should|they should|can you|could you|would you|please)\b/,EN=[["cost",/\b(cost|costs|costing|spend|spent|spending|price|money|bill|budget|tokens?|dollars?)\b|\$|how much (has|did|does|is|will)\b/],["problem",/\b(problems?|issues?|wrong|stuck|broken|errors?|fail|failed|failing|ok|okay|fine|alright|all right|slow|hang|hanging|hung|blocked|trouble|not working|(so|too) long|taking (so )?long)\b/],["when",/\b(when|how long|eta|ready|finish|finished|done|time left|soon|how much longer|how many minutes|take)\b/],["next",/\b(next|after this|after that|then what|what comes|coming up)\b/],["doing",/\b(doing|working|status|progress|where is|where's|happening|happened|up to|right now|how('s| is) it going|what (did|has) (it|he|she|they))\b/]],AR_POLITE=/^(لو سمحت|من فضلك|رجاء)?\s*(ممكن\s+)?(تقول لي|تقولي|قل لي|قول لي|قلي|خبرني|اخبرني|ابي اعرف|ابغي اعرف|ابغا اعرف|اريد ان اعرف|اريد اعرف|ودي اعرف)\s+/,AR_DO=/^(خل|خلي|خليه|غير|اضف|ضيف|زود|احذف|شيل|امسح|حط|ضع|اجعل|بدل|عدل|سو|سوي|اكتب|ابي|ابغي|ابغا|اريد|نبي|نبغي|لازم|يجب|ممكن|استخدم|اعرض|اخف)(\s|$)/,AR=[["cost",/(تكلف|كلف|تكاليف|سعر|فلوس|مصاريف|مصروف|صرف|دفع|توكن|دولار)/],["when",/(متي|امتي|ايمتي|كم باقي|كم يحتاج|كم ياخذ|كم وقت|كم دقيقه|يخلص|تخلص|خلص|ينتهي|تنتهي|انتهي|جاهز|يجهز|تجهز)/],["problem",/(مشكل|عالق|معلق|واقف|متوقف|خطا|غلط|تمام|سليم|بطي|متاخر|طول|طولت|عطل|خربان|في شي|فيه شي)/],["next",/(التالي|التاليه|الجاي|الجايه|القادم|القادمه|بعدين|بعد كذا|بعد ذلك|بعدها|وش بعد|ايش بعد|شو بعد)/],["doing",/(يسوي|تسوي|يعمل|تعمل|يشتغل|تشتغل|يفعل|تفعل|الحاله|وين وصل|فين وصل|وصل لوين|وش يصير|ايش يصير|شو عم|الحين|حاليا)/]];function askTopic(text3){if(isArabic(text3)){let s2=plainArabic(text3).replace(/[؟?!.،,]/g," ").replace(/\s+/g," ").trim(),q5=s2.replace(AR_POLITE,"");return q5!==s2?{topic:AR.find(([,re])=>re.test(q5))?.[0]??"other",lang:"ar"}:AR_DO.test(q5)?{topic:"change",lang:"ar"}:{topic:AR.find(([,re])=>re.test(q5))?.[0]??"other",lang:"ar"}}let s=text3.toLowerCase().replace(/[’`]/g,"'").replace(/[?!.,;:]+/g," ").replace(/\s+/g," ").trim(),q4=s.replace(EN_POLITE,"");if(q4===s&&EN_DO.test(q4))return{topic:"change",lang:"en"};let found2=EN.find(([,re])=>re.test(q4))?.[0];return found2?{topic:found2,lang:"en"}:{topic:q4===s&&EN_ASK.test(q4)?"change":"other",lang:"en"}}var WHO_AR={analyst:"\u0627\u0644\u0645\u062D\u0644\u0644",architect:"\u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A",builder:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631",reviewer:"\u0627\u0644\u0645\u0631\u0627\u062C\u0639",tester:"\u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631",squad:"squad-kit"},YOUR_PART_AR={questions:"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0627\u0644\u0623\u0633\u0626\u0644\u0629",spec:"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A",plan:"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u062A\u0635\u0645\u064A\u0645",proof:"\u0645\u0631\u0627\u062C\u0639\u0629 \u0627\u0644\u0635\u0648\u0631 \u0648\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u064A\u0647\u0627",stuck:"\u0627\u062E\u062A\u064A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629",release:"\u0625\u0635\u062F\u0627\u0631 \u0627\u0644\u0646\u0633\u062E\u0629",ask:"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0627\u0644\u0633\u0624\u0627\u0644",yours:"\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0625\u0644\u0649 \u0627\u0644\u0641\u0631\u064A\u0642",chat:"\u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629",change:"\u0627\u0644\u0628\u062A \u0641\u064A \u062A\u062D\u062F\u064A\u062B \u0627\u0644\u062A\u0635\u0645\u064A\u0645",depends:"\u0627\u062E\u062A\u064A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0627\u0644\u0628\u0646\u0627\u0621"},NEED_AR={"answer its questions, or approve the spec":"\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0644\u0649 \u0623\u0633\u0626\u0644\u062A\u0647 \u0623\u0648 \u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A","approve the spec":"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A","approve the design":"\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u0649 \u0627\u0644\u062A\u0635\u0645\u064A\u0645","check the proof":"\u0645\u0631\u0627\u062C\u0639\u0629 \u0627\u0644\u0635\u0648\u0631 \u0648\u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629 \u0639\u0644\u064A\u0647\u0627"};function arMinutes(seconds2){let m=Math.max(1,Math.round(seconds2/60));return m===1?"\u062F\u0642\u064A\u0642\u0629 \u0648\u0627\u062D\u062F\u0629":m===2?"\u062F\u0642\u064A\u0642\u062A\u064A\u0646":m<=10?`${m} \u062F\u0642\u0627\u0626\u0642`:`${m} \u062F\u0642\u064A\u0642\u0629`}function arIn(seconds2){return Math.round(seconds2/60)<=1?"\u062E\u0644\u0627\u0644 \u062F\u0642\u064A\u0642\u0629 \u062A\u0642\u0631\u064A\u0628\u064B\u0627":`\u062E\u0644\u0627\u0644 ${arMinutes(seconds2)} \u062A\u0642\u0631\u064A\u0628\u064B\u0627`}function arRange(u){if(u.high<60)return"\u0623\u0642\u0644 \u0645\u0646 \u062F\u0642\u064A\u0642\u0629";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`\u062D\u0648\u0627\u0644\u064A ${arMinutes(low*60)}`:`\u0645\u0646 ${low} \u0625\u0644\u0649 ${high} ${high<=10?"\u062F\u0642\u0627\u0626\u0642":"\u062F\u0642\u064A\u0642\u0629"}`}var VERB_AR=[[/^editing /,"\u064A\u0639\u062F\u0651\u0644 "],[/^reading /,"\u064A\u0642\u0631\u0623 "],[/^writing /,"\u064A\u0643\u062A\u0628 "],[/^running /,"\u064A\u0634\u063A\u0651\u0644 "],[/^searching for /,"\u064A\u0628\u062D\u062B \u0639\u0646 "],[/^looking for /,"\u064A\u0628\u062D\u062B \u0639\u0646 "],[/^looking up /,"\u064A\u0628\u062D\u062B \u0641\u064A "],[/^listing /,"\u064A\u0633\u062A\u0639\u0631\u0636 "]];function doingAr(doing){for(let[re,ar]of VERB_AR)if(re.test(doing))return doing.replace(re,ar);return doing}var SENTENCE_AR={questions:"\u0627\u0644\u0645\u062D\u0644\u0644 \u064A\u0642\u0631\u0623 \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0644\u064A\u0639\u0631\u0641 \u0645\u0627 \u064A\u062D\u062A\u0627\u062C \u0623\u0646 \u064A\u0633\u0623\u0644\u0643 \u0639\u0646\u0647.",spec:"\u0627\u0644\u0645\u062D\u0644\u0644 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A: \u0627\u0644\u0642\u0635\u0629 \u0648\u0634\u0631\u0648\u0637 \u0627\u0644\u0625\u0646\u062C\u0627\u0632.",design:"\u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A \u064A\u062E\u062A\u0627\u0631 \u0637\u0631\u064A\u0642\u0629 \u0627\u0644\u0628\u0646\u0627\u0621 \u0648\u064A\u0643\u062A\u0628 \u0627\u0644\u062A\u0635\u0645\u064A\u0645.",build:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0643\u062A\u0628 \u0627\u0644\u0643\u0648\u062F.",fix:"\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0635\u0644\u062D \u0645\u0627 \u0648\u062C\u062F\u062A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0623\u0648 \u0627\u0644\u0645\u0631\u0627\u062C\u0639.",checks:"squad-kit \u064A\u0634\u063A\u0651\u0644 \u0641\u062D\u0648\u0635\u0627\u062A \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0628\u0646\u0641\u0633\u0647\u060C \u0641\u0644\u0627 \u064A\u0642\u064A\u0651\u0645 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0645\u0644\u0647 \u0628\u0646\u0641\u0633\u0647.",review:"\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u060C \u0648\u0647\u0648 \u0648\u0643\u064A\u0644 \u062C\u062F\u064A\u062F\u060C \u064A\u0642\u0631\u0623 \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A.",proof:"\u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u064A\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0648\u064A\u0644\u062A\u0642\u0637 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632."};function screens2(t){return changesScreens(t)}var MERGES_AR="\u064A\u0642\u0628\u0644 \u0627\u0644\u0641\u0631\u064A\u0642 \u0627\u0644\u0639\u0645\u0644 \u0648\u064A\u062F\u0645\u062C\u0647 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627.";function thenAr(t,now2){let shots=screens2(t)?"\u060C \u062B\u0645 \u064A\u0644\u062A\u0642\u0637 \u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632":"",ready=t.track==="auto"?MERGES_AR:"\u0627\u0644\u0625\u062B\u0628\u0627\u062A \u062C\u0627\u0647\u0632 \u0644\u062A\u0631\u0627\u062C\u0639\u0647.",p=now2.part;switch(now2.step){case"questions":return"\u0633\u064A\u0633\u0623\u0644\u0643 \u0627\u0644\u0645\u062D\u0644\u0644 \u0639\u0645\u0627 \u064A\u062D\u062A\u0627\u062C \u0645\u0639\u0631\u0641\u062A\u0647\u060C \u0623\u0648 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0645\u0628\u0627\u0634\u0631\u0629.";case"spec":return"\u062A\u0642\u0631\u0623 \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u0627\u060C \u062B\u0645 \u064A\u0643\u062A\u0628 \u0627\u0644\u0645\u0639\u0645\u0627\u0631\u064A \u0627\u0644\u062A\u0635\u0645\u064A\u0645.";case"design":return t.track==="express"?"\u0627\u0644\u0645\u0633\u0627\u0631 \u0627\u0644\u0633\u0631\u064A\u0639: \u062A\u064F\u0639\u062A\u0645\u062F \u0627\u0644\u062E\u0637\u0629 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0648\u062A\u0644\u064A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0648\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u0629.":t.track==="auto"?"\u0645\u0633\u0627\u0631 \xABBuilds by itself\xBB: \u062A\u064F\u0639\u062A\u0645\u062F \u0627\u0644\u062E\u0637\u0629 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0648\u062A\u0644\u064A\u0647 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0648\u0627\u0644\u0645\u0631\u0627\u062C\u0639\u0629.":"\u062A\u0642\u0631\u0623 \u0627\u0644\u062A\u0635\u0645\u064A\u0645 \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u060C \u062B\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0645\u0637\u0648\u0651\u0631.";case"build":return p&&p.n<p.of?`\u064A\u0643\u0645\u0644 \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0627\u0644\u062E\u0637\u0648\u0629 ${p.n+1} \u0645\u0646 ${p.of}\u060C \u062B\u0645 \u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0648\u064A\u0642\u0631\u0623 \u0645\u0631\u0627\u062C\u0639 \u062C\u062F\u064A\u062F \u0627\u0644\u0639\u0645\u0644${shots}.`:`\u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0648\u064A\u0642\u0631\u0623 \u0645\u0631\u0627\u062C\u0639 \u062C\u062F\u064A\u062F \u0627\u0644\u0639\u0645\u0644${shots}.`;case"fix":return`\u062A\u0639\u0645\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0645\u0631\u0629 \u0623\u062E\u0631\u0649 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627\u060C \u062B\u0645 \u064A\u0642\u0631\u0623 \u0627\u0644\u0645\u0631\u0627\u062C\u0639 \u0627\u0644\u0639\u0645\u0644${shots}.`;case"checks":return`\u064A\u0642\u0631\u0623 \u0627\u0644\u0645\u0631\u0627\u062C\u0639\u060C \u0648\u0647\u0648 \u0648\u0643\u064A\u0644 \u062C\u062F\u064A\u062F\u060C \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A${shots}.`;case"review":return screens2(t)?"\u064A\u0644\u062A\u0642\u0637 \u0627\u0644\u0645\u062E\u062A\u0628\u0650\u0631 \u0635\u0648\u0631\u0629 \u0644\u0643\u0644 \u0634\u0631\u0637 \u0625\u0646\u062C\u0627\u0632.":ready;default:return ready}}function noteLeft(now2){return now2.usual?Math.max(60,middle(now2.usual)-(now2.seconds??0)):60}function noteWhenAr(t,now2){let none="\u0644\u0627 \u064A\u0639\u0645\u0644 \u0623\u064A \u0648\u0643\u064A\u0644 \u0627\u0644\u0622\u0646\u060C \u0641\u0644\u0646 \u064A\u0642\u0631\u0623\u0647\u0627 \u0623\u062D\u062F \u0628\u0639\u062F.";switch(now2.state){case"working":return now2.role==="squad"?`squad-kit \u064A\u0634\u063A\u0651\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0627\u0644\u0622\u0646. \u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0648\u0643\u064A\u0644 \u0627\u0644\u062A\u0627\u0644\u064A \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647\u060C ${arIn(noteLeft(now2))}.`:`\u064A\u0642\u0631\u0624\u0647\u0627 ${WHO_AR[now2.role??"builder"]} \u0639\u0646\u062F \u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C ${arIn(noteLeft(now2))}.`;case"you":return`${none} \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F\u0645\u0627 \u064A\u062A\u0627\u0628\u0639 \u0627\u0644\u0639\u0645\u0644\u060C \u0628\u0639\u062F ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`;case"queued":return`${none} \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631: \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646 \u0648\u0628\u062F\u0621 \u062E\u0637\u0648\u062A\u0647\u0627.`;case"stopping":return"\u0627\u0644\u0641\u0631\u064A\u0642 \u064A\u062A\u0648\u0642\u0641 \u0627\u0644\u0622\u0646\u060C \u0648\u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0639\u0646\u062F \u0645\u062A\u0627\u0628\u0639\u0629 \u0627\u0644\u0639\u0645\u0644.";case"ready":case"done":return"\u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644\u060C \u0641\u0644\u0627 \u064A\u0639\u0645\u0644 \u0639\u0644\u064A\u0647 \u0623\u064A \u0648\u0643\u064A\u0644 \u0628\u0639\u062F \u0627\u0644\u0622\u0646. \u062A\u0628\u0642\u0649 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629 \u0641\u064A \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0644\u0644\u0633\u062C\u0644.";default:return`${none} \u0633\u064A\u0642\u0631\u0624\u0647\u0627 \u0627\u0644\u0641\u0631\u064A\u0642 \u0639\u0646\u062F \u0628\u062F\u0621 \u0627\u0644\u0639\u0645\u0644 \u0639\u0644\u0649 \u0627\u0644\u062A\u0630\u0643\u0631\u0629.`}}function canPropose(t){return t.stage==="release"||t.stage==="done"||!!t.specApproved||["implement","test","review"].includes(t.stage)}function agentOf(now2){return now2.role&&now2.role!=="squad"?now2.role:"analyst"}function slowNow(now2,book,t){return now2.slow?!0:!now2.step||!now2.role||now2.seconds===void 0?!1:now2.seconds>=slowAfter(usualOf(book,now2.role,now2.step,t.track))}function costLine(t,lang2){let total=totalOf(t.usage);return total.runs?total.costUsd===void 0?lang2==="ar"?`\u0644\u0645 \u062A\u0630\u0643\u0631 \u0623\u062F\u0627\u0629 \u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064A \u062A\u0643\u0644\u0641\u0629 \u0647\u0630\u0647 \u0627\u0644\u062A\u0634\u063A\u064A\u0644\u0627\u062A (${total.runs}).`:`The AI tool did not report a cost for these ${total.runs} runs.`:lang2==="ar"?`\u0627\u0644\u062A\u0643\u0644\u0641\u0629 \u062D\u062A\u0649 \u0627\u0644\u0622\u0646 ${total.costUsd<.005?"\u0623\u0642\u0644 \u0645\u0646 0.01 \u062F\u0648\u0644\u0627\u0631":`${total.costUsd.toFixed(2)} \u062F\u0648\u0644\u0627\u0631`} \u0628\u0623\u0633\u0639\u0627\u0631 API\u060C \u0641\u064A ${total.runs} ${total.runs<=10?"\u0645\u0631\u0627\u062A \u062A\u0634\u063A\u064A\u0644":"\u0645\u0631\u0629 \u062A\u0634\u063A\u064A\u0644"}.`:`So far it has cost ${costWords(total)} at API prices, over ${total.runs} ${total.runs===1?"run":"runs"}.`:lang2==="ar"?"\u0644\u0645 \u064A\u064F\u062D\u0633\u0628 \u0634\u064A\u0621 \u0628\u0639\u062F: \u0644\u0645 \u064A\u0639\u0645\u0644 \u0623\u064A \u0648\u0643\u064A\u0644 \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062A\u0630\u0643\u0631\u0629.":"Nothing is counted yet: no agent has run on this ticket."}function problemLines(t,now2,book,lang2){let ar=lang2==="ar",secs=now2.seconds;if(now2.state==="working"){let who4=ar?WHO_AR[now2.role??"builder"]:`The ${now2.who}`;if(now2.slow?.kind==="stuck"){let quiet=now2.slow.text;return ar?[`\u0642\u062F \u064A\u0643\u0648\u0646 \u0627\u0644\u0639\u0645\u0644 \u0639\u0627\u0644\u0642\u064B\u0627. ${quiet.startsWith("No progress")?"\u0644\u0627 \u062A\u0642\u062F\u0651\u0645 \u0645\u0646\u0630 \u0641\u062A\u0631\u0629.":""}`.trim(),"\u0627\u0644\u0628\u0637\u0627\u0642\u0629 \u0623\u0639\u0644\u0627\u0647 \u062A\u0639\u0631\u0636 \u0645\u0627 \u0641\u062D\u0635\u0647 squad-kit \u0648\u0637\u0631\u0642 \u0627\u0644\u0645\u062A\u0627\u0628\u0639\u0629."]:[`It may be stuck. ${quiet}`,"The card above says what squad-kit checked, with the ways out."]}if(slowNow(now2,book,t))return!now2.slow&&!ar?[`It is slower than usual, but still working. ${now2.role==="squad"?"squad-kit":who4} has been on this step for ${minutesWords(secs??0)}. It usually takes ${rangeEn(now2.usual)}.`,"You can give it more time, stop and try again, or stop it."]:ar?[`\u0627\u0644\u0639\u0645\u0644 \u0623\u0628\u0637\u0623 \u0645\u0646 \u0627\u0644\u0645\u0639\u062A\u0627\u062F\u060C \u0644\u0643\u0646\u0647 \u0645\u0633\u062A\u0645\u0631. ${who4} \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u0645\u0646\u0630 ${arMinutes(secs??0)}\u060C \u0648\u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(now2.usual)}.`,"\u064A\u0645\u0643\u0646\u0643 \u0645\u0646 \u0627\u0644\u0628\u0637\u0627\u0642\u0629 \u0623\u0639\u0644\u0627\u0647 \u0645\u0646\u062D\u0647 10 \u062F\u0642\u0627\u0626\u0642 \u0625\u0636\u0627\u0641\u064A\u0629\u060C \u0623\u0648 \u0625\u064A\u0642\u0627\u0641\u0647 \u0648\u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0646 \u062C\u062F\u064A\u062F\u060C \u0623\u0648 \u0625\u064A\u0642\u0627\u0641\u0647."]:[`It is slower than usual, but still working. ${now2.slow?.text??""}`.trim(),"You can give it 10 more minutes, stop and try again, or stop it from the card above."];let p=now2.part,u=now2.usual,checks=now2.role==="squad",first2=ar?checks?`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. squad-kit \u064A\u0634\u063A\u0651\u0644 \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A${secs!==void 0?` \u0645\u0646\u0630 ${arMinutes(secs)}`:" \u0627\u0644\u0622\u0646"}. \u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(u)}.`:`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ${who4}${p?` \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${p.n} \u0645\u0646 ${p.of} \u0648`:" "}\u064A\u0639\u0645\u0644${secs!==void 0?` \u0645\u0646\u0630 ${arMinutes(secs)}`:" \u0627\u0644\u0622\u0646"}. \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u062A\u0633\u062A\u063A\u0631\u0642 \u0639\u0627\u062F\u0629 ${arRange(u)}.`:checks?`No problem. squad-kit is running the checks${secs!==void 0?`, for ${minutesWords(secs)} so far`:""}. They usually take ${rangeEn(u)}.`:secs===void 0?`No problem. ${who4} is working${p?` on step ${p.n} of ${p.of}`:""}. This step usually takes ${rangeEn(u)}.`:`No problem. ${who4}${p?` is on step ${p.n} of ${p.of} and has worked for`:" has worked on this step for"} ${minutesWords(secs)}. This step usually takes ${rangeEn(u)}.`,tries=(t.tries??0)>0?ar?"\u0641\u0634\u0644\u062A \u0627\u0644\u0641\u062D\u0648\u0635\u0627\u062A \u0645\u0631\u0629 \u0639\u0644\u0649 \u0627\u0644\u0637\u0631\u064A\u0642\u060C \u0648\u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u064A\u0635\u0644\u062D\u0647\u0627.":`The checks failed ${t.tries===1?"once":`${t.tries} times`} on the way; the Builder fixes what they found.`:"",then=ar?`\u0628\u0639\u062F\u0647\u0627: ${thenAr(t,now2)} \u0633\u062A\u0638\u0647\u0631 \u0641\u064A \xABWaiting for you\xBB \u0639\u0646\u062F\u0645\u0627 \u062A\u062D\u062A\u0627\u062C\u0643.`:`Next: ${lowerFirst3(now2.then??"")} I will put it in Waiting for you when it needs you.`;return[first2,tries,then].filter(Boolean)}return now2.state==="you"?t.waiting?.kind==="stuck"?ar?["\u0646\u0639\u0645: \u062A\u0648\u0642\u0641 \u0627\u0644\u0641\u0631\u064A\u0642 \u0648\u064A\u062D\u062A\u0627\u062C \u0642\u0631\u0627\u0631\u0643.",t.stuck?.question??""].filter(Boolean):["Yes: the squad stopped and needs your decision.",t.stuck?.question??""].filter(Boolean):ar?[`\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. \u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u062A\u0646\u062A\u0638\u0631 \u0645\u0646\u0643 ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[`No problem. ${now2.sentence}`]:ar?[stateAr(t,now2,"\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629. ")]:[`No problem. ${now2.sentence}`]}function stateAr(t,now2,lead=""){switch(now2.state){case"queued":return`${lead}\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631\u060C \u0648\u062A\u0628\u062F\u0623 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646.`;case"stopping":return"\u0627\u0644\u0639\u0645\u0644 \u064A\u062A\u0648\u0642\u0641 \u0643\u0645\u0627 \u0637\u0644\u0628\u062A. \u0643\u0644 \u0645\u0627 \u0623\u064F\u0646\u062C\u0632 \u0645\u062D\u0641\u0648\u0638.";case"ready":return`${lead}\u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644 \u0648\u064A\u0635\u062F\u0631 \u0645\u0639 ${t.release??"\u0627\u0644\u0646\u0633\u062E\u0629 \u0627\u0644\u0642\u0627\u062F\u0645\u0629"}.`;case"done":return`${lead}\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`;case"you":return`${lead}\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u062A\u0646\u062A\u0638\u0631 \u0645\u0646\u0643 ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`;default:return`${lead}\u0644\u0627 \u064A\u062C\u0631\u064A \u0623\u064A \u0639\u0645\u0644 \u0639\u0644\u064A\u0647\u0627 \u0627\u0644\u0622\u0646.`}}function rangeEn(u){if(u.high<60)return"under a minute";let low=Math.max(1,Math.round(u.low/60)),high=Math.max(low,Math.round(u.high/60));return low===high?`about ${low} minute${low===1?"":"s"}`:`${low} to ${high} minutes`}function lowerFirst3(s){return/^[A-Z][A-Z0-9]+\b/.test(s)?s:s.charAt(0).toLowerCase()+s.slice(1)}function whenLines(t,now2,book,lang2){let ar=lang2==="ar";if(now2.state==="working"&&now2.readyIn){if(slowNow(now2,book,t)){let who4=ar?WHO_AR[now2.role??"builder"]:now2.role==="squad"?"squad-kit":`The ${now2.who}`;return ar?[`\u0627\u0644\u0639\u0645\u0644 \u0623\u0628\u0637\u0623 \u0645\u0646 \u0627\u0644\u0645\u0639\u062A\u0627\u062F\u060C \u0644\u0630\u0644\u0643 \u064A\u0635\u0639\u0628 \u0627\u0644\u062A\u0642\u062F\u064A\u0631. ${who4} \u0639\u0644\u0649 \u0647\u0630\u0647 \u0627\u0644\u062E\u0637\u0648\u0629 \u0645\u0646\u0630 ${arMinutes(now2.seconds??0)}.`]:[`It is taking longer than usual, so it is hard to say. ${now2.slow?.text??`${who4} has been on this step for ${minutesWords(now2.seconds??0)}.`}`]}let what=now2.readyIn.what;return t.track==="auto"?ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 ${MERGES_AR}`]:[`Probably ${inAbout(now2.readyIn.seconds)}. Then it is accepted and merged by itself.`]:ar?[`\u063A\u0627\u0644\u0628\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[what]??what}.`]:[`Probably ${inAbout(now2.readyIn.seconds)}. Then you will be asked to ${what}.`]}return now2.state==="you"?ar?[`\u0627\u0644\u062A\u0630\u0643\u0631\u0629 \u0628\u0627\u0646\u062A\u0638\u0627\u0631\u0643 \u0627\u0644\u0622\u0646: \u062A\u062D\u062A\u0627\u062C ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[`It is waiting for you now: ${lowerFirst3(t.waiting?.summary??"give it back when you are done")}.`]:now2.state==="queued"?ar?["\u0644\u0645 \u062A\u0628\u062F\u0623 \u062E\u0637\u0648\u062A\u0647\u0627 \u0628\u0639\u062F: \u0647\u064A \u0627\u0644\u062A\u0627\u0644\u064A\u0629 \u0641\u064A \u0627\u0644\u062F\u0648\u0631\u060C \u0648\u062A\u0628\u062F\u0623 \u0639\u0646\u062F \u062A\u0648\u0641\u0631 \u0645\u0643\u0627\u0646."]:["It is next in line, so its step has not started yet. It starts when a slot is free."]:now2.state==="ready"?ar?[`\u062C\u0627\u0647\u0632\u0629: \u0627\u0644\u0639\u0645\u0644 \u0645\u0642\u0628\u0648\u0644 \u0648\u064A\u0635\u062F\u0631 \u0645\u0639 ${t.release??"\u0627\u0644\u0646\u0633\u062E\u0629 \u0627\u0644\u0642\u0627\u062F\u0645\u0629"}.`]:[`It is ready: accepted, and it goes out with ${t.release??"the next release"}.`]:now2.state==="done"?ar?[`\u0635\u062F\u0631\u062A${t.release?` \u0641\u064A ${t.release}`:""}.`]:[now2.sentence]:now2.state==="idle"?ar?["\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0639\u0645\u0644 \u0639\u0644\u064A\u0647\u0627 \u0628\u0639\u062F."]:["Work on it has not started yet."]:ar?[stateAr(t,now2)]:[now2.sentence]}function doingLines(t,now2,lang2){if(lang2==="en")return[now2.sentence,...now2.state==="working"&&now2.doing?[`Right now it is ${now2.doing}.`]:[]];if(now2.state!=="working")return[stateAr(t,now2)];let who4=WHO_AR[now2.role??"builder"],title2=now2.part?t.steps?.[now2.part.n-1]?.title:void 0;return[now2.part&&title2?`${who4} \u0641\u064A \u0627\u0644\u062E\u0637\u0648\u0629 ${now2.part.n} \u0645\u0646 ${now2.part.of}: ${title2}.`:SENTENCE_AR[now2.step??"build"],...now2.doing?[`\u0627\u0644\u0622\u0646 ${doingAr(now2.doing)}.`]:[]]}function nextLines(t,now2,lang2){let ar=lang2==="ar";return now2.state==="working"&&now2.readyIn?ar?t.track==="auto"?[thenAr(t,now2),`\u064A\u064F\u0642\u0628\u0644 \u0627\u0644\u0639\u0645\u0644 \u0648\u064A\u064F\u062F\u0645\u062C \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 ${arIn(now2.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[thenAr(t,now2),`\u0628\u0639\u062F\u0647\u0627 \u0633\u0646\u0637\u0644\u0628 \u0645\u0646\u0643 ${NEED_AR[now2.readyIn.what]??now2.readyIn.what} ${arIn(now2.readyIn.seconds)}. \u0644\u0627 \u0634\u064A\u0621 \u0645\u0637\u0644\u0648\u0628 \u0645\u0646\u0643 \u0627\u0644\u0622\u0646.`]:[now2.next??""]:now2.state==="you"?ar?[`\u064A\u062A\u0627\u0628\u0639 \u0627\u0644\u0641\u0631\u064A\u0642 \u0628\u0639\u062F ${t.waiting?YOUR_PART_AR[t.waiting.kind]:YOUR_PART_AR.yours}.`]:[now2.next??now2.sentence]:ar?[stateAr(t,now2)]:[now2.next??now2.sentence]}function answerAsk(t,now2,book,question){let{topic,lang:lang2}=askTopic(question),ar=lang2==="ar",who4=agentOf(now2),to=now2.state==="working"&&now2.role&&now2.role!=="squad"?now2.role:void 0,note2={do:"note",label:ar?`\u0623\u0631\u0633\u0644\u0647\u0627 \u0645\u0644\u0627\u062D\u0638\u0629 \u0625\u0644\u0649 ${to?WHO_AR[to]:"\u0627\u0644\u0641\u0631\u064A\u0642"}`:`Send it as a note to the ${to?WHO[to]:"squad"}`,note:ar?noteWhenAr(t,now2):now2.noteWhen,...to?{to}:{}},chat={do:"chat",label:ar?`\u0627\u0633\u0623\u0644 ${WHO_AR[who4]}`:`Ask the ${WHO[who4]}`,to:who4},stop={do:"stop",label:ar?"\u0623\u0648\u0642\u0641 \u0627\u0644\u0639\u0645\u0644":"Stop the work"},answer2=(lines3,offers=[],instant=!0)=>({lang:lang2,topic,instant,lines:lines3.filter(Boolean),offers});switch(topic){case"problem":return answer2(problemLines(t,now2,book,lang2),now2.state==="working"&&!now2.slow?[stop]:[]);case"when":return answer2(whenLines(t,now2,book,lang2));case"doing":return answer2(doingLines(t,now2,lang2));case"next":return answer2(nextLines(t,now2,lang2));case"cost":return answer2([costLine(t,lang2)]);case"change":{if(t.stage==="release"||t.stage==="done"){let line3=ar?`${t.id} \u0645\u0642\u0628\u0648\u0644\u0629\u060C \u0644\u0630\u0644\u0643 \u064A\u0635\u0628\u062D \u0623\u064A \u062A\u063A\u064A\u064A\u0631 \u062A\u0630\u0643\u0631\u0629 \u062C\u062F\u064A\u062F\u0629 \u0645\u0631\u062A\u0628\u0637\u0629 \u0628\u0647\u0627 \u0623\u0648 \u0628\u0644\u0627\u063A \u062E\u0637\u0623.`:`${t.id} is accepted, so a change becomes a new linked ticket or a bug report.`;return answer2([line3],[{do:"propose",label:ar?"\u0627\u0637\u0644\u0628 \u062A\u063A\u064A\u064A\u0631\u064B\u0627":"Ask for a change",note:ar?"\u062A\u062E\u062A\u0627\u0631: \u062A\u0630\u0643\u0631\u0629 \u062C\u062F\u064A\u062F\u0629 \u0645\u0631\u062A\u0628\u0637\u0629 \u0623\u0648 \u0628\u0644\u0627\u063A \u062E\u0637\u0623.":"You choose: a new linked ticket or a bug report."}],!1)}return canPropose(t)?answer2([ar?"\u0647\u0630\u0627 \u064A\u063A\u064A\u0651\u0631 \u0645\u0627 \u0648\u0627\u0641\u0642\u062A \u0639\u0644\u064A\u0647 \u0641\u064A \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A. \u0643\u064A\u0641 \u062A\u0631\u064A\u062F \u0623\u0646 \u064A\u0635\u0644 \u0625\u0644\u0649 \u0627\u0644\u0641\u0631\u064A\u0642\u061F":"That changes what you approved in the spec. How should it reach the squad?"],[{do:"propose",label:ar?"\u0627\u0642\u062A\u0631\u062D \u062A\u0639\u062F\u064A\u0644\u064B\u0627":"Propose a change",note:ar?"\u062A\u064F\u062D\u062F\u064E\u0651\u062B \u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0648\u062A\u0648\u0627\u0641\u0642 \u0639\u0644\u064A\u0647\u0627\u060C \u062B\u0645 \u064A\u0639\u064A\u062F \u0627\u0644\u0645\u0637\u0648\u0651\u0631 \u0645\u0627 \u062A\u063A\u064A\u0651\u0631 \u0641\u0642\u0637.":"The spec is updated, you approve it, and the Builder redoes only what changed."},note2],!1):answer2([ar?"\u0627\u0644\u0645\u0648\u0627\u0635\u0641\u0627\u062A \u0644\u0645 \u062A\u064F\u0639\u062A\u0645\u062F \u0628\u0639\u062F\u060C \u0641\u0645\u0627 \u0632\u0627\u0644 \u0627\u0644\u0641\u0631\u064A\u0642 \u064A\u0633\u062A\u0637\u064A\u0639 \u0623\u062E\u0630 \u0630\u0644\u0643 \u0641\u064A \u0627\u0644\u062D\u0633\u0628\u0627\u0646. \u0623\u0631\u0633\u0644\u0647\u0627 \u0645\u0644\u0627\u062D\u0638\u0629\u060C \u0623\u0648 \u0646\u0627\u0642\u0634\u0647\u0627 \u0645\u0639 \u0627\u0644\u0645\u062D\u0644\u0644.":"The spec is not approved yet, so the squad can still take this in. Send it as a note, or talk it through with the Analyst."],[note2,{do:"chat",label:ar?"\u0646\u0627\u0642\u0634\u0647\u0627 \u0645\u0639 \u0627\u0644\u0645\u062D\u0644\u0644":"Talk it through with the Analyst",to:"analyst"}],!1)}default:return answer2([ar?"\u0623\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0641\u0648\u0631\u064B\u0627 \u0639\u0646 \u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u062D\u0627\u0644\u0629: \u0647\u0644 \u062A\u0648\u062C\u062F \u0645\u0634\u0643\u0644\u0629\u060C \u0645\u062A\u0649 \u064A\u062C\u0647\u0632\u060C \u0645\u0627\u0630\u0627 \u064A\u0639\u0645\u0644 \u0627\u0644\u0622\u0646\u060C \u0645\u0627 \u0627\u0644\u062E\u0637\u0648\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629\u060C \u0648\u0643\u0645 \u0643\u0644\u0651\u0641. \u0644\u0623\u064A \u0633\u0624\u0627\u0644 \u0622\u062E\u0631 \u0627\u0633\u0623\u0644 \u0623\u062D\u062F \u0627\u0644\u0648\u0643\u0644\u0627\u0621.":"I answer status questions at once: is there a problem, when will it be ready, what is it doing, what is next, and what it has cost. For anything else, ask an agent."],[chat],!1)}}import path137 from"path";var STEP_NAME={questions:"Questions",spec:"Spec",design:"Design",build:"Build",fix:"Fixes",checks:"Checks",review:"Review",proof:"Proof"},SESSION={questions:"intake",spec:"intake",design:"intake",build:"build",fix:"build",checks:void 0,review:"review",proof:"tester"},ENDS=[{step:"questions",who:"analyst",by:["analyst"],re:/^Asked \d+ questions?$/},{step:"spec",who:"analyst",by:["analyst"],re:/^(Wrote|Updated) the spec\b/},{step:"design",who:"architect",by:["architect","spec"],re:/^(Wrote|Updated) the (design|plan)\b/},{step:"build",who:"builder",by:["builder"],re:/^Finished all steps$/},{step:"fix",who:"builder",by:["builder"],re:/^Fixed (the|what)\b/},{step:"checks",who:"squad",by:["tester"],re:/^(All (\d+ )?checks pass|Checks failed|Checks still fail|No test, lint or build commands)/},{step:"review",who:"reviewer",by:["reviewer"],re:/^(Approved\b|Asked for \d+ fix|Still asks for)/},{step:"proof",who:"tester",by:["tester"],re:/^(Saved \d+ screenshots?|No screenshots were taken|No screens to show|No screens change)/}],AFTER={questions:[{re:/^Answered \d+ questions?/,who:["you"],note:"you answered"}],spec:[{re:/^Approved the spec/,who:["you"],note:"you approved"}],design:[{re:/^Approved the (design|plan)/,who:["you"],note:"you approved"},{re:/^Express track: approved the plan on its own/,who:["squad"],note:"approved on its own (Express track)"},{re:/^Builds by itself: approved the plan on its own/,who:["squad"],note:"approved on its own (Builds by itself)"}],proof:[{re:/^Checked the proof\. Looks right/,who:["you"],note:"you checked it: Looks right"},{re:/^(.+ passed: a|A)ccepted by itself/,who:["squad"],note:"accepted by itself"}]},TOOLISH=/^(Reading|Running|Editing|Writing|Looking|Searching|Listing|Runs on|Asked|Started|Finished|squad-kit|The answer was not complete)\b/,REFUSED=/User refused permission to run tool|^Permission denied: |haven't granted it yet/i,ms=iso2=>iso2?Date.parse(iso2):NaN;function oldestFirst(t){return[...t.activity].sort((a,b)=>a.at.localeCompare(b.at))}function runsOf2(t,log3){let history=t.history??[],first2=history.length?ms(history[0].from):1/0,fromLog=[];for(let a of log3){if(ms(a.at)>=first2)break;let end=ENDS.find(e=>e.by.includes(a.who)&&e.re.test(a.text));end&&fromLog.push({who:end.who,step:end.step,to:a.at,line:a})}let timed=history.map(h=>({...h,line:endLine(log3,h)}));return[...fromLog,...timed]}function endLine(log3,h){let end=ENDS.find(e=>e.step===h.step),lo=ms(h.from)-1e3,hi=ms(h.to)+6e4;return log3.find(a=>ms(a.at)>=lo&&ms(a.at)<=hi&&end.by.includes(a.who)&&end.re.test(a.text))}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run6){let end=ms(run6.line?.at??run6.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run6.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf(t,run6,log3){let line3=run6.line?.text??"";switch(run6.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read6=analystSummary(log3,run6),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read6?`${read6.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q4=>q4.answer).length,points=t.criteria?.length??0;return`Wrote the spec${/from your talk/.test(line3)?" from your talk":answers2?` from your ${plural4(answers2,"answer")}`:""}${points?`, with ${plural4(points,'"done when" point')}`:""}.`}case"design":{if(/^Updated the (design|plan): /.test(line3))return line3.replace(/\.?$/,".");let steps=t.steps?.length??0,said=t.decision?.options.find(o=>o.chosen)?.name?.replace(/^(A|An|The|One|Its|Their|Our|Your)\b/,w=>w.toLowerCase()),parts2=[said?`Chose ${said}`:"Wrote the design",steps?`planned ${plural4(steps,"step")} to build`:"",t.mockup?"made a mockup":""].filter(Boolean);return`${parts2.length>1?`${parts2.slice(0,-1).join(", ")} and ${parts2.at(-1)}`:parts2[0]}.`}case"build":{let steps=t.steps?.length??0;return steps?`Did the ${plural4(steps,"step")} of the design.`:"Built the change."}case"fix":return line3?line3.replace(/\.?$/,"."):"Fixed what the checks or the review found.";case"checks":return/^All/.test(line3)?`squad-kit ran the project's checks: ${line3.replace(/^All /,"all ").replace(/\.?$/,".")}`:line3?`squad-kit ran the project's checks. ${line3.replace(/\.?$/,".")}`:"squad-kit ran the project's checks.";case"review":return/^Approved: /.test(line3)?line3.replace(/\.?$/,"."):/^Approved/.test(line3)?"Approved the changes.":line3?`${line3.split(":")[0]}.`:"Read the changes.";case"proof":{let n=t.proof?.length??0,pictures=(t.proof??[]).reduce((sum,p)=>sum+1+(p.more?.length??0),0);return pictures>n?`Opened the app and took ${plural4(pictures,"screenshot")} of the ${plural4(n,'"done when" point')} it could show.`:n?`Opened the app and took ${plural4(n,"screenshot")}, one for each "done when" point it could show.`:line3?line3.replace(/\.?$/,"."):"Checked the change."}}}function filesFrom(conv,cwd,from,to){let added2=new Set,changed=new Set;if(!conv)return{added:[],changed:[]};let lo=ms(from),hi=ms(to),rel=p=>cwd&&p.startsWith(`${cwd}/`)?p.slice(cwd.length+1):path137.basename(p);for(let e of conv.updates){let at3=ms(e.at);if(Number.isFinite(lo)&&Number.isFinite(at3)&&(at3<lo||at3>hi))continue;let u=e.update;if(!(u.sessionUpdate!=="tool_call"&&u.sessionUpdate!=="tool_call_update"))for(let c of u.content??[]){if(c.type!=="diff"||!c.path)continue;let p=rel(c.path);c.oldText==null&&!changed.has(p)?added2.add(p):added2.has(p)||changed.add(p)}}return{added:[...added2],changed:[...changed]}}function handedIn(t,run6,files,conv){switch(run6.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q4=>({from:q4.from,text:q4.text,...q4.answer?{answer:q4.answer.value?`${q4.answer.choice.replace(/:$/,"")}: ${q4.answer.value}`:q4.answer.choice}:{}}))}:void 0;case"spec":return t.story||t.criteria?.length?{kind:"spec",...t.story?{story:t.story}:{},criteria:(t.criteria??[]).map(c=>c.then)}:void 0;case"design":{if(!t.decision&&!t.steps?.length)return;let chosen2=t.decision?.options.find(o=>o.chosen)?.name;return{kind:"design",...t.decision?{title:t.decision.title}:{},...chosen2?{chosen:chosen2}:{},...t.decision?.adr?{adr:t.decision.adr}:{},steps:(t.steps??[]).map(s=>s.title),mockup:!!t.mockup,follows:t.follows??[]}}case"build":case"fix":{let mine2=run6.step==="fix"||!files?filesFrom(conv,conv?.cwd,run6.from,run6.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run6.step==="build"?(t.steps??[]).filter(s=>s.status==="done").map(s=>s.title):[];return!got.added.length&&!got.changed.length&&!got.removed.length&&!steps.length?void 0:{kind:"files",added:got.added,changed:got.changed,removed:got.removed,steps}}case"checks":return t.gates?.checks&&t.gates.checks!=="pending"?{kind:"checks",ok:t.gates.checks==="pass"||t.gates.checks==="skipped",text:t.gates.checksText??(t.gates.checks==="pass"?"All checks pass":"Some checks failed")}:void 0;case"review":{let line3=run6.line?.text??"",approved=/^Approved/.test(line3)||!line3&&t.gates?.review==="pass",notes=approved?[]:t.reviewNotes?.length?t.reviewNotes:line3.includes(": ")?[line3.slice(line3.indexOf(": ")+2)]:[];return{kind:"verdict",approved,notes}}case"proof":return t.proof?.length?{kind:"shots",shots:t.proof.flatMap(p=>[p.after,...(p.more??[]).map(m=>m.after)].filter(file3=>!file3.endsWith(".api.json")).map(file3=>({criterion:p.criterion,file:file3})))}:void 0}}var TAB={spec:"plan",design:"plan",build:"changes",fix:"changes",checks:"proof",proof:"proof"};function counts2(conv,from,to){if(!conv||!from)return null;let lo=ms(from),hi=to?ms(to):1/0,inside5=at3=>{let x=ms(at3);return Number.isFinite(x)&&x>=lo-1e3&&x<=hi+1e3},actions=0,skipped=0;for(let e of conv.updates){if(!inside5(e.at))continue;let u=e.update;u.sessionUpdate==="tool_call"&&(actions+=1),u.sessionUpdate==="tool_call_update"&&u.status==="failed"&&(u.content??[]).some(c=>c.type==="content"&&c.content?.type==="text"&&REFUSED.test(c.content.text??""))&&(skipped+=1)}return{actions,skipped}}function whatHappened(bp,t,src={}){let now2=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read6=src.conversation??(id2=>readConversation(id2,3e3,void 0,bp.chatsDir)),convs=new Map,conv=key=>{let id2=key?t.sessions?.[key]:void 0;if(!id2)return null;if(!convs.has(id2))try{convs.set(id2,read6(id2))}catch{convs.set(id2,null)}return convs.get(id2)??null},files,ticketFiles=()=>{if(files===void 0){try{files=src.files?.()??null}catch{files=null}files&&!files.added.length&&!files.changed.length&&!files.removed.length&&(files=null)}return files},latest=new Map;runs2.forEach((r,i)=>latest.set(r.step,i));let reviews=runs2.filter(r=>r.step==="review").length,steps=runs2.map((run6,i)=>{let next=runs2[i+1],session=SESSION[run6.step],c=conv(session),isLatest=latest.get(run6.step)===i,seconds2=run6.from?Math.max(0,(ms(run6.to)-ms(run6.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run6.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run6.to)-1e3&&ms(l.at)<=until)&&notes.push(a.note);run6.step==="review"&&notes.push(reviews>1?`round ${runs2.slice(0,i+1).filter(r=>r.step==="review").length} of ${reviews}, a fresh session`:"a second, separate session");let got=isLatest?handedIn(t,run6,run6.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run6.from,run6.to),tab=TAB[run6.step];return{who:run6.who,step:run6.step,name:`${WHO[run6.who]} \xB7 ${STEP_NAME[run6.step]}`,...run6.from?{from:run6.from}:{},to:run6.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run6.step==="build"&&changed?summaryOf(t,run6,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf(t,run6,log3),...got?{handedIn:got}:{},...got&&tab?{tab}:{},notes,...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}}}),ck=t.clock;if(ck&&!t.waiting){let from=ck.began??ck.since,session=SESSION[ck.step],n=counts2(conv(session),from),part=ck.step==="build"&&ck.part&&t.steps?.length?` Step ${ck.part} of ${t.steps.length}.`:"";steps.push({who:ck.who,step:ck.step,name:`${WHO[ck.who]} \xB7 ${STEP_NAME[ck.step]}`,from,seconds:Math.max(0,(now2-ms(from))/1e3),running:!0,summary:`${(ck.doing??t.status)||"Working on it"}${ck.doing?".":""}${part}`.replace(/\.\./g,"."),notes:[],...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}})}return{steps,seconds:steps.reduce((s,x)=>s+(x.seconds??0),0)}}async function body2(c){try{return await c.req.json()}catch{return{}}}async function changedFiles3(bp,id2){let{files}=await ticketChanges(bp,id2);return files.length?{added:files.filter(f=>f.status==="added").map(f=>f.path),changed:files.filter(f=>f.status==="changed"||f.status==="renamed").map(f=>f.path),removed:files.filter(f=>f.status==="deleted").map(f=>f.path)}:null}function mountCostApi(app,bp){app.get("/api/v1/tickets/:id/happened",async c=>{let t=readTicket(bp,c.req.param("id"));if(!t)return c.json({error:"not found"},404);let files=await changedFiles3(bp,t.id).catch(()=>null);return c.json(whatHappened(bp,t,{files:()=>files}))}),app.get("/api/v1/tickets/:id/cost",async c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketCost(bp,t,await howPaid(bp))):c.json({error:"not found"},404)}),app.get("/api/v1/cost",async c=>boardExists(bp)?c.json(costOverview(bp,await howPaid(bp))):c.json({error:"No board here yet."},404)),app.put("/api/v1/cost/payment",async c=>{let b=await body2(c);return b.payment!=="subscription"&&b.payment!=="api-key"&&b.payment!=="auto"?c.json({error:"Pick subscription, api-key or auto."},400):(setPaymentChoice(b.payment),c.json(costOverview(bp,await howPaid(bp))))}),app.put("/api/v1/cost/budget",async c=>{let b=await body2(c);try{let change={};"ticketUsd"in b&&(change.ticketUsd=parseUsd(b.ticketUsd)),"monthUsd"in b&&(change.monthUsd=parseUsd(b.monthUsd));let budget=setBudget(bp,change);return c.json({...costOverview(bp,await howPaid(bp)),budget})}catch(err2){return c.json({error:plainWords(err2,"Saving the budget")},400)}})}import{createHash as createHash12}from"crypto";import fs145 from"fs";import path138 from"path";function docsHash(bp,id2){let said=["spec.md","design.md","questions.md","proof.md"].flatMap(f=>{try{return[f,fs145.readFileSync(path138.join(ticketDir(bp,id2),f),"utf8")]}catch{return[]}});return said.length?createHash12("sha256").update(said.join("\0")).digest("hex").slice(0,16):NO_DOCS}var MOST_HASHED=20*1024*1024;async function workOf2(meta,t){let hash2=createHash12("sha256"),built=!1;for(let c of copiesOf(meta,t)){if(!c.baseCommit||!fs145.existsSync(c.dir))continue;let files=(await workSoFar(c.dir,c.baseCommit)).sort();if(files.length){built=!0,hash2.update(await gitLater(c.dir,["rev-parse","HEAD"],{allowFail:!0}));for(let file3 of files){hash2.update(`\0${file3}\0`);try{let full=path138.join(c.dir,file3),seen=await fs145.promises.stat(full);hash2.update(seen.size>MOST_HASHED?`${seen.size}:${seen.mtimeMs}`:await fs145.promises.readFile(full))}catch{}}}}return{built,hash:built?hash2.digest("hex").slice(0,16):"none"}}function refuseLeaving(bp,t,meta){if(t.takenOver)throw new BoardActionError(`${t.id} is yours already. Give it back first.`);if(!isMine(t,meta))throw new BoardActionError(`${t.id} is ${ownerOf(t)}'s. Only they can move it.`);if(t.deleting)throw new BoardActionError(`${t.id} is being deleted.`);if(t.archived)throw new BoardActionError(`${t.id} is archived.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already. Report a bug or add a new ticket instead.`);if(t.chat?.open&&!t.chat.beside)throw new BoardActionError(`${t.id} has a chat open. Finish the talk first.`);let change=openChange(t);if(change)throw new BoardActionError(`${t.id} has a change still open (${change.id}). Settle it first.`);if(squadAlive(bp)&&(t.live||t.squad||t.stopRequested||t.questions?.some(q4=>q4.thinking)))throw new BoardActionError(`${t.id} is working right now. Wait until it needs you, or press Stop first.`)}async function workOutside(bp,id2){refuseWhileSaving(bp,id2);let meta=readMeta(bp);if(meta.engine!=="live")throw new BoardActionError("The sample board has no real project to work on outside.");let before=requireTicket(bp,id2);if(before.takenOver?.free)return before;refuseLeaving(bp,before,meta);let work=await workOf2(meta,before),docs=docsHash(bp,id2);return updateTicket(bp,id2,t=>{t.takenOver?.free||(refuseLeaving(bp,t,meta),leaveForOutside(t,meta.user.name,new Date().toISOString(),{docs,work:work.hash}),addActivity(t,"you","Moved it to a conversation outside squad-kit. The squad waits"))})}function asItWas(t,left){t.stage=left.stage,t.status=left.status,t.waiting=left.waiting,t.live=!1,left.phase&&!left.waiting&&(t.live=!0,scheduleSquad(t,left.phase,.2,1)),addActivity(t,"you","Brought it back. Nothing changed outside")}var BUILT_OUTSIDE="Built outside squad-kit, and squad-kit has not checked it";async function bringBack2(bp,id2){let meta=readMeta(bp),before=requireTicket(bp,id2);if(!before.takenOver?.free)throw new BoardActionError(`${before.id} is not in a conversation outside squad-kit.`);let work=await workOf2(meta,before),docs=docsHash(bp,id2),approvedBy=`${meta.user.name} (outside squad-kit)`,approveDesign=!1,back=updateTicket(bp,id2,t=>{let taken=t.takenOver,left=taken?.free;if(!taken||!left)throw new BoardActionError(`${t.id} is back already.`);if(t.takenOver=void 0,t.stopRequested=void 0,t.retryOnStop=void 0,t.waiting=null,t.squad=null,docs===left.docs&&work.hash===left.work&&!left.approved)return asItWas(t,left);t.outside=t.outside?{...t.outside,checked:void 0}:{...left.from?{from:left.from}:{},by:taken.by,at:taken.at},t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.offScreen=void 0,t.proofRound=void 0,t.live=!1,work.built?(left.designApproved&&(t.decision=t.decision&&{...t.decision,adr:t.decision.adr??saveDesignDecision(bp,t,approvedBy)}),t.steps=t.steps?.map(s=>({...s,status:"done"})),t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="review",t.status="Back from outside. squad-kit has not checked it",t.waiting={kind:"proof",summary:BUILT_OUTSIDE},addActivity(t,"you","Brought it back with code built outside squad-kit")):hasDesign(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a design written outside squad-kit"),left.designApproved?(approveDesign=!0,t.status="Design approved in the conversation",t.waiting={kind:"plan",summary:"The design was approved in the conversation"}):t.track==="express"||t.track==="auto"?(t.live=!0,t.status="Approving the plan",scheduleSquad(t,"express-approve",.2,1)):(t.status="Design ready for you",t.waiting={kind:"plan",summary:"The design was written outside squad-kit: is this how to build it?"})):hasSpec(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a spec written outside squad-kit"),needsSpec(t)?(t.status="Spec ready for you",t.waiting={kind:"spec",summary:"The spec was written outside squad-kit: is this what you want?"}):(t.live=!0,t.status="Architect is writing the design",scheduleSquad(t,"plan-write",.2,1))):(t.stage="tickets",t.live=!0,t.status="Analyst is reading the ticket",addActivity(t,"you","Brought it back. The squad carries on from the start"),scheduleSquad(t,"analyst-read",.2,1))});return approveDesign?approve(bp,back.id):back}function recordApproval(bp,id2,what){let meta=readMeta(bp);return updateTicket(bp,id2,t=>{let left=t.takenOver?.free;if(!left)throw new BoardActionError(`${t.id} is not in a conversation outside squad-kit.`);if(what==="spec"?!hasSpec(t):!hasDesign(t))throw new BoardActionError(`${t.id} has no ${what} written yet, so there is nothing to approve.`);t.specApproved??={by:`${meta.user.name} (outside squad-kit)`,at:new Date().toISOString()},left.approved=!0,what==="design"&&(left.designApproved=!0),addActivity(t,"you",`Approved the ${what} in the conversation outside squad-kit`)})}function checkOutside(bp,id2){return refuseWhileSaving(bp,id2),updateTicket(bp,id2,t=>{if(!t.outside)throw new BoardActionError(`${t.id} was not worked on outside squad-kit.`);if(t.takenOver)throw new BoardActionError(`${t.id} is still in the conversation. Bring it back first.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already.`);if(!waitsAtReview(t))throw new BoardActionError(`${t.id} has no built work waiting to be checked right now.`);if(!t.criteria?.length)throw new BoardActionError(`${t.id} has no "Done when" list in its spec.md, so there is nothing to check it against. Add one first.`);t.outside={...t.outside,checked:void 0},t.waiting=null,t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.proofRound=void 0,t.live=!0,t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="test",t.status="Running the project checks on the work done outside",addActivity(t,"you","Asked squad-kit to check the work done outside"),scheduleSquad(t,"test",.2,1)})}import{execFileSync as execFileSync16}from"child_process";import fs146 from"fs";import path139 from"path";var GUIDE_FILE="how-to-work-here.md",SAMPLE_DOCS=ticketDocs({id:"SK-1",title:"Sign in with a phone number",story:"People with no email can sign in with their phone number.",criteria:[{given:"someone on the sign-in page",when:"they enter their phone number and the code sent to it",then:"they are signed in"},{given:"someone on the sign-in page",when:"they enter a wrong code three times",then:"they wait ten minutes before trying again"}],releaseNote:"You can now sign in with your phone number.",questions:[{id:"q1",from:"analyst",text:"Should a code expire?",options:["After 5 minutes","After 30 minutes"],answer:{choice:"After 5 minutes"}}],decision:{title:"Where the codes are kept",area:"Data",options:[{name:"In the database",note:"Survives a restart",chosen:!0},{name:"In memory",note:"Simple, but lost on a restart"}],why:"A code must still work after a restart."},steps:[{title:"Add the codes table",status:"todo"},{title:"Send a code and check it",status:"todo"},{title:"Add the phone number to the sign-in page",status:"todo"}]});function shape(file3){return`\`\`\`\`markdown
1836
1839
  ${SAMPLE_DOCS[file3].trimEnd()}
1837
1840
  \`\`\`\``}function commandsOf(bp){let meta=readMeta(bp),repos=reposOf(meta);return repos.flatMap(r=>{let said=Object.entries(r.commands??{}).filter(([,v])=>typeof v=="string"&&v.trim()),where=repos.length>1?`${r.path}: `:"";return said.length?said.map(([k,v])=>`- ${where}${k}: \`${String(v).trim()}\``):[`- ${where}none written down yet. Find them in the project's own files.`]}).join(`
1838
1841
  `)}function guideText(bp){return`# How to work on this project in a conversation